30

Vue中使用装饰器,我是认真的

 3 years ago
source link: https://segmentfault.com/a/1190000023471570
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

产品上线事繁多,测试产品催不离。

作为一个曾经的 Java coder , 当我第一次看到 js 里面的装饰器( Decorator )的时候,就马上想到了 Java 中的注解,当然在实际原理和功能上面, Java 的注解和 js 的装饰器还是有很大差别的。本文题目是 Vue中使用装饰器,我是认真的 ,但本文将从装饰器的概念开发聊起,一起来看看吧。

通过本文内容,你将学到以下内容:

class
Vue

本文首发于公众号【前端有的玩】,不想当咸鱼,想要换工作,关注公众号,带你每日一起刷大厂面试题,关注 === 大厂 offer

什么是装饰器

装饰器是 ES2016 提出来的一个提案,当前处于 Stage 2 阶段,关于装饰器的体验,可以点击 https://github.com/tc39/proposal-decorators 查看详情。装饰器是一种与类相关的语法糖,用来包装或者修改类或者类的方法的行为,其实装饰器就是设计模式中装饰者模式的一种实现方式。不过前面说的这些概念太干了,我们用人话来翻译一下,举一个例子。

在日常开发写 bug 过程中,我们经常会用到防抖和节流,比如像下面这样

class MyClass {
  follow = debounce(function() {
    console.log('我是子君,关注我哦')
  }, 100)
}

const myClass = new MyClass()
// 多次调用只会输出一次
myClass.follow()
myClass.follow()

上面是一个防抖的例子,我们通过 debounce 函数将另一个函数包起来,实现了防抖的功能,这时候再有另一个需求,比如希望在调用 follow 函数前后各打印一段日志,这时候我们还可以再开发一个 log 函数,然后继续将 follow 包装起来

/**
 * 最外层是防抖,否则log会被调用多次
 */
class MyClass {
  follow = debounce(
    log(function() {
      console.log('我是子君,关注我哦')
    }),
    100
  )
}

上面代码中的 debouncelog 两个函数,本质上是两个包装函数,通过这两个函数对原函数的包装,使原函数的行为发生了变化,而 js 中的装饰器的原理就是这样的,我们使用装饰器对上面的代码进行改造

class MyClass {
  @debounce(100)
  @log
  follow() {
    console.log('我是子君,关注我哦')
  }
}

装饰器的形式就是 @ + 函数名 ,如果有参数的话,后面的括号里面可以传参

在方法上使用装饰器

装饰器可以应用到 class 上或者 class 里面的属性上面,但一般情况下,应用到 class 属性上面的场景会比较多一些,比如像上面我们说的 log , debounce 等等,都一般会应用到类属性上面,接下来我们一起来具体看一下如何实现一个装饰器,并应用到类上面。在实现装饰器之前,我们需要先了解一下属性描述符

了解一下属性描述符

在我们定义一个对象里面的属性的时候,其实这个属性上面是有许多属性描述符的,这些描述符标明了这个属性能不能修改,能不能枚举,能不能删除等等,同时 ECMAScript 将这些属性描述符分为两类,分别是数据属性和访问器属性,并且数据属性与访问器属性是不能共存的。

数据属性

数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性包含了四个描述符,分别是

  1. configurable

    表示能不能通过 delete 删除属性,能否修改属性的其他描述符特性,或者能否将数据属性修改为访问器属性。当我们通过 let obj = {name: ''} 声明一个对象的时候,这个对象里面所有的属性的 configurable 描述符的值都是 true

  2. enumerable

    表示能不能通过 for in 或者 Object.keys 等方式获取到属性,我们一般声明的对象里面这个描述符的值是 true ,但是对于 class 类里面的属性来说,这个值是 false

  3. writable

    表示能否修改属性的数据值,通过将这个修改为 false ,可以实现属性只读的效果。

  4. value

    表示当前属性的数据值,读取属性值的时候,从这里读取;写入属性值的时候,会写到这个位置。

访问器属性

访问器属性不包含数据值,他们包含了 gettersetter 两个函数,同时 configurableenumerable 是数据属性与访问器属性共有的两个描述符。

  1. getter

    在读取属性的时候调用这个函数,默认这个函数为 undefined

  2. setter

    在写入属性值的时候调用这个函数,默认这个函数为 undefined

了解了这六个描述符之后,你可能会有几个疑问: 我如何去定义修改这些属性描述符?这些属性描述符与今天的文章主题有什么关系?接下来是揭晓答案的时候了。

使用 Object.defineProperty

了解过 vue2.0 双向绑定原理的同学一定知道, Vue 的双向绑定就是通过使用 Object.defineProperty 去定义数据属性的 gettersetter 方法来实现的,比如下面有一个对象

let obj = {
  name: '子君',
  officialAccounts: '前端有的玩'
}

我希望这个对象里面的用户名是不能被修改的,用 Object.defineProperty 该如何定义呢?

Object.defineProperty(obj,'name', {
  // 设置writable 是 false, 这个属性将不能被修改
  writable: false
})
// 修改obj.name
obj.name = "君子"
// 打印依然是子君
console.log(obj.name)

通过 Object.defineProperty 可以去定义或者修改对象属性的属性描述符,但是因为数据属性与访问器属性是互斥的,所以一次只能修改其中的一类,这一点需要注意。

定义一个防抖装饰器

装饰器本质上依然是一个函数,不过这个函数的参数是固定的,如下是防抖装饰器的代码

/**
*@param wait 延迟时长
*/
function debounce(wait) {
  return function(target, name, descriptor) {
    descriptor.value = debounce(descriptor.value, wait)
  }
}
// 使用方式
class MyClass {
  @debounce(100)
  follow() {
    console.log('我是子君,我的公众号是 【前端有的玩】,关注有惊喜哦')
  }
}

我们逐行去分析一下代码

  1. 首先我们定义了一个 debounce 函数,同时有一个参数 wait ,这个函数对应的就是在下面调用装饰器时使用的 @debounce(100)
  2. debounce 函数返回了一个新的函数,这个函数即装饰器的核心,这个函数有三个参数,下面逐一分析

    1. target : 这个类属性函数是在谁上面挂载的,如上例对应的是 MyClass
    2. name : 这个类属性函数的名称,对应上面的 follow
    3. descriptor : 这个就是我们前面说的属性描述符,通过直接 descriptor 上面的属性,即可实现属性只读,数据重写等功能
  3. 然后第三行 descriptor.value = debounce(descriptor.value, wait) , 前面我们已经了解到,属性描述符上面的 value 对应的是这个属性的值,所以我们通过重写这个属性,将其用 debounce 函数包装起来,这样在函数调用 follow 时实际调用的是包装后的函数

通过上面的三步,我们就实现了类属性上面可使用的装饰器,同时将其应用到了类属性上面

class 上使用装饰器

装饰器不仅可以应用到类属性上面,还可以直接应用到类上面,比如我希望可以实现一个类似 Vue 混入那样的功能,给一个类混入一些方法属性,应该如何去做呢?

// 这个是要混入的对象
const methods = {
  logger() {
    console.log('记录日志')
  }
}

// 这个是一个登陆登出类
class Login{
  login() {}
  logout() {}
}

如何将上面的 methods 混入到 Login 中,首先我们先实现一个类装饰器

function mixins(obj) {
  return function (target) {
    Object.assign(target.prototype, obj)  
  }
}

// 然后通过装饰器混入
@mixins(methods)
class Login{
  login() {}
  logout() {}
}

这样就实现了类装饰器。对于类装饰器,只有一个参数,即 target ,对应的就是这个类本身。

了解完装饰器,我们接下来看一下如何在 Vue 中使用装饰器。

Vue 中使用装饰器

使用 ts 开发 Vue 的同学一定对 vue-property-decorator 不会感到陌生,这个插件提供了许多装饰器,方便大家开发的时候使用,当然本文的中点不是这个插件。其实如果我们的项目没有使用 ts ,也是可以使用装饰器的,怎么用呢?

配置基础环境

除了一些老的项目,我们现在一般新建 Vue 项目的时候,都会选择使用脚手架 vue-cli3/4 来新建,这时候新建的项目已经默认支持了装饰器,不需要再配置太多额外的东西,如果你的项目使用了 eslint ,那么需要给 eslint 配置以下内容。

parserOptions: {
    ecmaFeatures:{
      // 支持装饰器
      legacyDecorators: true
    }
  }

使用装饰器

虽然 Vue 的组件,我们一般书写的时候 export 出去的是一个对象,但是这个并不影响我们直接在组件中使用装饰器,比如就拿上例中的 log 举例。

function log() {
  /**
   * @param target 对应 methods 这个对象
   * @param name 对应属性方法的名称
   * @param descriptor 对应属性方法的修饰符
   */
  return function(target, name, descriptor) {
    console.log(target, name, descriptor)
    const fn = descriptor.value
    descriptor.value = function(...rest) {
      console.log(`这是调用方法【${name}】前打印的日志`)
      fn.call(this, ...rest)
      console.log(`这是调用方法【${name}】后打印的日志`)
    }
  }
}

export default {
  created() {
    this.getData()
  },
  methods: {
    @log()
    getData() {
      console.log('获取数据')
    }
  }
}

看了上面的代码,是不是发现在 Vue 中使用装饰器还是很简单的,和在 class 的属性上面使用的方式一模一样,但有一点需要注意,在 methods 里面的方法上面使用装饰器,这时候装饰器的 target 对应的是 methods

除了在 methods 上面可以使用装饰器之外,你也可以在生命周期钩子函数上面使用装饰器,这时候 target 对应的是整个组件对象。

一些常用的装饰器

下面小编罗列了几个小编在项目中常用的几个装饰器,方便大家使用

1. 函数节流与防抖

函数节流与防抖应用场景是比较广的,一般使用时候会通过 throttledebounce 方法对要调用的函数进行包装,现在就可以使用上文说的内容将这两个函数封装成装饰器, 防抖节流使用的是 lodash 提供的方法,大家也可以自行实现节流防抖函数哦

import { throttle, debounce } from 'lodash'
/**
 * 函数节流装饰器
 * @param {number} wait 节流的毫秒
 * @param {Object} options 节流选项对象
 * [options.leading=true] (boolean): 指定调用在节流开始前。
 * [options.trailing=true] (boolean): 指定调用在节流结束后。
 */
export const throttle =  function(wait, options = {}) {
  return function(target, name, descriptor) {
    descriptor.value = throttle(descriptor.value, wait, options)
  }
}

/**
 * 函数防抖装饰器
 * @param {number} wait 需要延迟的毫秒数。
 * @param {Object} options 选项对象
 * [options.leading=false] (boolean): 指定在延迟开始前调用。
 * [options.maxWait] (number): 设置 func 允许被延迟的最大值。
 * [options.trailing=true] (boolean): 指定在延迟结束后调用。
 */
export const debounce = function(wait, options = {}) {
  return function(target, name, descriptor) {
    descriptor.value = debounce(descriptor.value, wait, options)
  }
}

封装完之后,在组件中使用

import {debounce} from '@/decorator'

export default {
  methods:{
    @debounce(100)
    resize(){}
  }
}

2. loading

在加载数据的时候,为了个用户一个友好的提示,同时防止用户继续操作,一般会在请求前显示一个loading,然后在请求结束之后关掉loading,一般写法如下

export default {
  methods:{
    async getData() {
      const loading = Toast.loading()
      try{
        const data = await loadData()
        // 其他操作
      }catch(error){
        // 异常处理
        Toast.fail('加载失败');
      }finally{
        loading.clear()
      }  
    }
  }
}

我们可以把上面的 loading 的逻辑使用装饰器重新封装,如下代码

import { Toast } from 'vant'

/**
 * loading 装饰器
 * @param {*} message 提示信息
 * @param {function} errorFn 异常处理逻辑
 */
export const loading =  function(message = '加载中...', errorFn = function() {}) {
  return function(target, name, descriptor) {
    const fn = descriptor.value
    descriptor.value = async function(...rest) {
      const loading = Toast.loading({
        message: message,
        forbidClick: true
      })
      try {
        return await fn.call(this, ...rest)
      } catch (error) {
        // 在调用失败,且用户自定义失败的回调函数时,则执行
        errorFn && errorFn.call(this, error, ...rest)
        console.error(error)
      } finally {
        loading.clear()
      }
    }
  }
}

然后改造上面的组件代码

export default {
  methods:{
    @loading('加载中')
    async getData() {
      try{
        const data = await loadData()
        // 其他操作
      }catch(error){
        // 异常处理
        Toast.fail('加载失败');
      }  
    }
  }
}

3. 确认框

当你点击删除按钮的时候,一般都需要弹出一个提示框让用户确认是否删除,这时候常规写法可能是这样的

import { Dialog } from 'vant'

export default {
  methods: {
    deleteData() {
      Dialog.confirm({
        title: '提示',
        message: '确定要删除数据,此操作不可回退。'
      }).then(() => {
        console.log('在这里做删除操作')
      })
    }
  }
}

我们可以把上面确认的过程提出来做成装饰器,如下代码

import { Dialog } from 'vant'

/**
 * 确认提示框装饰器
 * @param {*} message 提示信息
 * @param {*} title 标题
 * @param {*} cancelFn 取消回调函数
 */
export function confirm(
  message = '确定要删除数据,此操作不可回退。',
  title = '提示',
  cancelFn = function() {}
) {
  return function(target, name, descriptor) {
    const originFn = descriptor.value
    descriptor.value = async function(...rest) {
      try {
        await Dialog.confirm({
          message,
          title: title
        })
        originFn.apply(this, rest)
      } catch (error) {
        cancelFn && cancelFn(error)
      }
    }
  }
}

然后再使用确认框的时候,就可以这样使用了

export default {
  methods: {
    // 可以不传参,使用默认参数
    @confirm()
    deleteData() {
      console.log('在这里做删除操作')
    }
  }
}

是不是瞬间简单多了,当然还可以继续封装很多很多的装饰器,因为文章内容有限,暂时提供这三个。

装饰器组合使用

在上面我们将类属性上面使用装饰器的时候,说道装饰器可以组合使用,在 Vue 组件上面使用也是一样的,比如我们希望在确认删除之后,调用接口时候出现 loading ,就可以这样写(一定要注意顺序)

export default {
  methods: {
    @confirm()
    @loading()
    async deleteData() {
      await delete()
    }
  }
}

本节定义的装饰器,均已应用到这个项目中 https://github.com/snowzijun/vue-vant-base , 这是一个基于 Vant 开发的开箱即用移动端框架,你只需要 fork 下来,无需做任何配置就可以直接进行业务开发,欢迎使用,喜欢麻烦给一个 star

我是子君,今天就写这么多,本文首发于【前端有的玩】,这是一个专注于前端技术,前端面试相关的公众号,同时关注之后即刻拉你加入前端交流群,我们一起聊前端,欢迎关注。

结语

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK