es7带来了很多更强大的方法,比如async/await,decorator等,相信大家对于async/await已经用的很熟练了,下面我们来讲一下decorator。

何为decorator?

官方说法,修饰器(Decorator)函数,用来修改类的行为。这样讲对于初学者来说不是很好理解,通俗点讲就是我们可以用修饰器来修改类的属性和方法,比如我们可以在函数执行之前改变它的行为。因为decorator是在编译时执行的,使得让我们能够在设计时对类、属性等进行标注和修改成为了可能。decorator不仅仅可以在类上面使用,还可以在对象上面使用,但是decorator不能修饰函数,因为函数存在变量提升。decorator相当于给对象内的函数包装一层行为。decorator本身就是一个函数,他有三个参数target(所要修饰的目标类), name(所要修饰的属性名), descriptor(该属性的描述对象)。后面我们会让大家体会到decorator的强大魅力。

大型框架都在使用decorator?
  • Angular2中的TypeScript Annotate就是标注装潢器的另一类实现。
  • React中redux2也开始利用ES7的Decorators进行了大量重构。
  • Vue如果你在使用typescript,你会发现vue组件也开始用Decorator了,就连vuex也全部用Decorators重构。
接下来让我们举一个简单的readonly的例子:

这是一个Dog类

class Dog {	
  bark () {	
    return '汪汪汪!!'	
  }	
}

让我们给他加上@readonly修饰器后

import { readOnly } from "./decorators";	
	
class Dog {	
  @readonly	
  bark () {	
    return '汪汪汪!!'	
  }	
}	
	
let dog = new Dog()	
dog.bark = 'wangwang!!';	
// Cannot assign to read only property 'bark' of [object Object]	
// 这里readonly修饰器把Dog类的bark方法修改为只读状态

让我们看下readonly是怎么实现的,代码很简单

/**	
 * @param target 目标类Dog	
 * @param name 所要修饰的属性名 bark	
 * @param descriptor 该属性的描述对象 bark方法	
 */	
function readonly(target, name, descriptor) {	
  // descriptor对象原来的值如下	
  // {	
  //   value: specifiedFunction,	
  //   enumerable: false,	
  //   configurable: true,	
  //   writable: true	
  // };	
  descriptor.writable = false;	
  return descriptor	
}

readonly有三个参数,第一个target是目标类Dog,第二个是所要修饰的属性名bark,是一个字符串,第三个是该属性的描述对象,bark方法。这里我们用readonly方法将bark方法修饰为只读。所以当你修改bark方法的时候就是报错了。

decorator 实用的decorator库 core-decorators.js

npm install core-decorators --save

// 将某个属性或方法标记为不可写。	
@readonly   	
// 标记一个属性或方法,以便它不能被删除; 也阻止了它通过Object.defineProperty被重新配置	
@nonconfigurable  	
// 立即将提供的函数和参数应用于该方法,允许您使用lodash提供的任意助手来包装方法。 第一个参数是要应用的函数,所有其他参数将传递给该装饰函数。	
@decorate  	
// 如果你没有像Babel 6那样的装饰器语言支持,或者甚至没有编译器的vanilla ES5代码,那么可以使用applyDecorators()助手。	
@extendDescriptor	
// 将属性标记为不可枚举。	
@nonenumerable	
// 防止属性初始值设定项运行,直到实际查找修饰的属性。	
@lazyInitialize	
// 强制调用此函数始终将此引用到类实例,即使该函数被传递或将失去其上下文。	
@autobind	
// 使用弃用消息调用console.warn()。 提供自定义消息以覆盖默认消息。	
@deprecate	
// 在调用装饰函数时禁止任何JavaScript console.warn()调用。	
@suppressWarnings	
// 将属性标记为可枚举。	
@enumerable	
// 检查标记的方法是否确实覆盖了原型链上相同签名的函数。	
@override  	
// 使用console.time和console.timeEnd为函数计时提供唯一标签,其默认前缀为ClassName.method。	
@time	
// 使用console.profile和console.profileEnd提供函数分析,并使用默认前缀为ClassName.method的唯一标签。	
@profile

还有很多这里就不过多介绍,了解更多 https://github.com/jayphelps/core-decorators

下面给大家介绍一些我们团队写的一些很实用的decorator方法库

作者:吴鹏和 罗学

  • noConcurrent 避免并发调用,在上一次操作结果返回之前,不响应重复操作
import {noConcurrent} from './decorators';	
methods: {	
  @noConcurrent     //避免并发,点击提交后,在接口返回之前无视后续点击	
  async onSubmit(){	
    let submitRes = await this.$http({...});	
    //...	
    return;	
  }	
}
  • makeMutex 多函数互斥,具有相同互斥标识的函数不会并发执行
import {makeMutex} from './decorators';	
let globalStore = {};	
class Navigator {	
  @makeMutex({namespace:globalStore, mutexId:'navigate'}) //避免跳转相关函数并发执行	
  static async navigateTo(route){...}	
	
  @makeMutex({namespace:globalStore, mutexId:'navigate'}) //避免跳转相关函数并发执行	
  static async redirectTo(route){...}	
}
  • withErrToast 捕获async函数中的异常,并进行错误提示
methods: {	
  @withErrToast({defaultMsg: '网络错误', duration: 2000})	
  async pullData(){	
    let submitRes = await this.$http({...});	
    //...	
    return '其他原因'; // toast提示 其他原因	
    // return 'ok';   // 正常无提示	
  }	
}
  • mixinList 用于分页加载,上拉加载时返回拼接数据及是否还有数据提示
methods: {	
  @mixinList({needToast: false})	
  async loadGoods(params = {}){	
    let goodsRes = await this.$http(params);	
    return goodsRes.respData.infos;	
  },	
  async hasMore() {	
    let result = await this.loadgoods(params);	
    if(result.state === 'nomore') this.tipText = '没有更多了';	
    this.goods = result.list;	
  }	
}	
// 上拉加载调用hasMore函数,goods数组就会得到所有拼接数据	
// loadGoods可传三个参数 params函数需要参数 ,startNum开始的页码,clearlist清空数组	
// mixinList可传一个参数 needToast 没有数据是否需要toast提示

typeCheck 检测函数参数类型

methods: {	
  @typeCheck('number')	
  btnClick(index){ ... },	
}	
// btnClick函数的参数index不为number类型 则报错

Buried 埋点处理方案,统计页面展现量和所有methods方法点击量,如果某方法不想设置埋点 可以 return 'noBuried' 即可

@Buried	
methods: { 	
  btn1Click() {	
    // 埋点为 btn1Click	
  },	
  btn2Click() {	
    return 'noBuried'; // 无埋点	
  },	
},	
created() {	
  // 埋点为 view	
}	
// 统计页面展现量和所有methods方法点击量

decorators.js

/**	
 * 避免并发调用,在上一次操作结果返回之前,不响应重复操作	
 * 如:用户连续多次点击同一个提交按钮,希望只响应一次,而不是同时提交多份表单	
 * 说明:	
 *    同步函数由于js的单线程特性没有并发问题,无需使用此decorator	
 *    异步时序,为便于区分操作结束时机,此decorator只支持修饰async函数	
 */	
export const noConcurrent = _noConcurrentTplt.bind(null,{mutexStore:'_noConCurrentLocks'});	
	
/**	
 * 避免并发调用修饰器模板	
 * @param {Object} namespace 互斥函数间共享的一个全局变量,用于存储并发信息,多函数互斥时需提供;单函数自身免并发无需提供,以本地私有变量实现	
 * @param {string} mutexStore 在namespace中占据一个变量名用于状态存储	
 * @param {string} mutexId   互斥标识,具有相同标识的函数不会并发执行,缺省值:函数名	
 * @param target	
 * @param funcName	
 * @param descriptor	
 * @private	
 */	
function _noConcurrentTplt({namespace={}, mutexStore='_noConCurrentLocks', mutexId}, target, funcName, descriptor) {	
  namespace[mutexStore] = namespace[mutexStore] || {};	
  mutexId = mutexId || funcName;	
	
  let oriFunc = descriptor.value;	
  descriptor.value = function () {	
    if (namespace[mutexStore][mutexId]) //上一次操作尚未结束,则无视本次调用	
      return;	
	
    namespace[mutexStore][mutexId] = true; //操作开始	
    let res = oriFunc.apply(this, arguments);	
	
    if (res instanceof Promise)	
      res.then(()=> {	
        namespace[mutexStore][mutexId] = false;	
      }).catch((e)=> {	
        namespace[mutexStore][mutexId] = false;	
        console.error(funcName, e);	
      }); //操作结束	
    else {	
      console.error('noConcurrent decorator shall be used with async function, yet got sync usage:', funcName);	
      namespace[mutexStore][mutexId] = false;	
    }	
	
    return res;	
  }	
}	
	
/**	
 * 多函数互斥,具有相同互斥标识的函数不会并发执行	
 * @param namespace 互斥函数间共享的一个全局变量,用于存储并发信息	
 * @param mutexId   互斥标识,具有相同标识的函数不会并发执行	
 * @return {*}	
 */	
export function makeMutex({namespace, mutexId}) {	
  if (typeof namespace !== "object") {	
    console.error('[makeNoConcurrent] bad parameters, namespace shall be a global object shared by all mutex funcs, got:', namespace);	
    return function () {}	
  }	
	
  return _noConcurrentTplt.bind(null, {namespace, mutexStore:'_noConCurrentLocksNS', mutexId})	
}	
	
/**	
 * 捕获async函数中的异常,并进行错误提示	
 * 函数正常结束时应 return 'ok',return其它文案时将toast指定文案,无返回值或产生异常时将toast默认文案	
 * @param {string} defaultMsg  默认文案	
 * @param {number, optional} duration 可选,toast持续时长	
 */	
export function withErrToast({defaultMsg, duration=2000}) {	
  return function (target, funcName, descriptor) {	
    let oriFunc = descriptor.value;	
    descriptor.value = async function () {	
      let errMsg = '';	
      let res = '';	
      try {	
        res = await oriFunc.apply(this, arguments);	
        if (res != 'ok')	
          errMsg = typeof res === 'string' ? res : defaultMsg;	
      } catch (e) {	
        errMsg = defaultMsg;	
        console.error('caught err with func:',funcName, e);	
      }	
	
      if (errMsg) {	
        this.$toast({	
          title: errMsg,	
          type: 'fail',	
          duration: duration,	
        });	
      }	
      return res;	
    }	
  }	
}	
	
/**	
 * 分页加载	
 * @param {[Boolean]} [是否加载为空显示toast]	
 * @return {[Function]} [decrotor]	
 */	
export function mixinList ({needToast = false}) {	
  let oldList = [],	
      pageNum = 1,	
  /**	
  * state [string]	
  *   hasmore  [还有更多]	
  *   nomore   [没有更多了]	
  */	
  state = 'hasmore',	
  current = [];	
  return function (target,name,descriptor) {	
    const oldFunc  = descriptor.value,	
          symbol   = Symbol('freeze');	
    target[symbol] = false;	
    /**	
     * [description]	
     * @param  {[Object]}   params={}       [请求参数]	
     * @param  {[Number]}   startNum=null   [手动重置加载页数]	
     * @param  {[Boolean]}  clearlist=false [是否清空数组]	
     * @return {[Object]}   [{所有加载页数组集合,加载完成状态}]	
     */	
    descriptor.value = async function(params={},startNum=null,clearlist=false) {	
      try {	
        if (target[symbol]) return;	
        // 函数执行前赋值操作	
        target[symbol] = true;	
        params.data.pageNum = pageNum;	
        if (startNum !== null && typeof startNum === 'number') {	
          params.data.pageNum = startNum;	
          pageNum = startNum;	
        }	
        if (clearlist) oldList = [];	
        // 释放函数,取回list	
        let before = current;	
        current = await oldFunc.call(this,params);	
        // 函数执行结束赋值操作	
        (state === 'hasmore' || clearlist) && oldList.push(...current);	
        if ((current.length === 0) || (params.data.pageSize > current.length)) {	
          needToast && this.$toast({title: '没有更多了',type: 'fail'});	
          state = 'nomore';	
        } else {	
          state = 'hasmore';	
          pageNum++;	
        }	
        target[symbol] = false;	
        this.$apply();	
        return { list : oldList,state };	
      } catch(e) {	
        console.error('fail code at: ' + e)	
      }	
    }	
  }	
}	
	
/**	
 * 检测工具	
 */ 	
const _toString = Object.prototype.toString;	
// 检测是否为纯粹的对象	
const _isPlainObject = function  (obj) {	
  return _toString.call(obj) === '[object Object]'	
}	
// 检测是否为正则	
const _isRegExp = function  (v) {	
  return _toString.call(v) === '[object RegExp]'	
}	
/**	
 * @description 检测函数	
 *  用于检测类型action	
 * @param {Array} checked 被检测数组	
 * @param {Array} checker 检测数组	
 * @return {Boolean} 是否通过检测	
 */ 	
const _check = function (checked,checker) {	
  check:	
  for(let i = 0; i < checked.length; i++) {	
    if(/(any)/ig.test(checker[i]))	
      continue check;	
    if(_isPlainObject(checked[i]) && /(object)/ig.test(checker[i]))	
      continue check;	
    if(_isRegExp(checked[i]) && /(regexp)/ig.test(checker[i]))	
      continue check;	
    if(Array.isArray(checked[i]) && /(array)/ig.test(checker[i]))	
      continue check;	
    let type = typeof checked[i];	
    let checkReg = new RegExp(type,'ig')	
    if(!checkReg.test(checker[i])) {	
      console.error(checked[i] + 'is not a ' + checker[i]);	
      return false;	
    }	
  }	
  return true;	
}	
/**	
 * @description 检测类型	
 *   1.用于校检函数参数的类型,如果类型错误,会打印错误并不再执行该函数;	
 *   2.类型检测忽略大小写,如string和String都可以识别为字符串类型;	
 *   3.增加any类型,表示任何类型均可检测通过;	
 *   4.可检测多个类型,如 "number array",两者均可检测通过。正则检测忽略连接符 ;	
 */	
export function typeCheck() {	
  const checker =  Array.prototype.slice.apply(arguments);	
  return function (target, funcName, descriptor) {	
    let oriFunc = descriptor.value;	
    descriptor.value =  function () {	
      let checked =  Array.prototype.slice.apply(arguments);	
      let result = undefined;	
      if(_check(checked,checker) ){	
        result = oriFunc.call(this,...arguments);	
      }	
      return result; 	
    }	
  }	
};	
	
const errorLog = (text) => {	
  console.error(text);	
  return true;	
}	
/**	
 * @description 全埋点 	
 *  1.在所有methods方法中埋点为函数名	
 *  2.在钩子函数中'beforeCreate','created','beforeMount','mounted','beforeUpdate','activated','deactivated'依次寻找这些钩子	
 *    如果存在就会增加埋点 VIEW	
 * 	
 * 用法: 	
 *   @Buried	
 *   在单文件导出对象一级子对象下;	
 *   如果某方法不想设置埋点 可以 return 'noBuried' 即可	
 */	
export function Buried(target, funcName, descriptor) {	
  let oriMethods = Object.assign({},target.methods),	
      oriTarget = Object.assign({},target);	
  // methods方法中	
  if(target.methods) {	
    for(let name in target.methods) {	
      target.methods[name] = function () {	
        let result = oriMethods[name].call(this,...arguments);	
        // 如果方法中返回 noBuried 则不添加埋点	
        if(typeof result === 'string' && result.includes('noBuried')) {	
          console.log(name + '方法设置不添加埋点');	
        } else if(result instanceof Promise) {	
          result.then(res => {	
            if(typeof res === 'string' && res.includes('noBuried')) { console.log(name + '方法设置不添加埋点'); return; };	
            console.log('添加埋点在methods方法中:' , name.toUpperCase ());	
            this.$log(name);	
          });	
        }else{	
          console.log('添加埋点在methods方法中:' , name.toUpperCase ());	
          this.$log(name);	
        };	
        return result;	
      }	
    }	
  }	
  // 钩子函数中	
  const hookFun = (hookName) => {	
    target[hookName] = function() {	
      let result =  oriTarget[hookName].call(this,...arguments);	
      console.log('添加埋点,在钩子函数' + hookName + '中:', 'VIEW');	
      this.$log('VIEW');	
      return result;	
    }	
  }	
	
  const LIFECYCLE_HOOKS = [	
    'beforeCreate',	
    'created',	
    'beforeMount',	
    'mounted',	
    'beforeUpdate',	
    'activated',	
    'deactivated',	
  ];	
	
  for(let item of LIFECYCLE_HOOKS) {	
    if (target[item]) return hookFun(item);	
  }	
}