乐趣区

关于前端:Vue中使用装饰器我是认真的

产品上线事繁多,测试产品催不离。
休问 Bug 剩多少,眼圈如漆身如泥。

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

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

  1. 理解什么是装璜器
  2. 在办法应用装璜器
  3. class 中应用装璜器
  4. 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

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

结语

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

退出移动版