产品上线事繁多,测试产品催不离。
休问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.nameobj.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

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

结语

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