es7你都懂了吗?今天带你了解es7的神器decorator

时间:2024-01-08 20:03:08

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类

  1. class Dog {

  2.  bark () {

  3.    return '汪汪汪!!'

  4.  }

  5. }

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

  1. import { readOnly } from "./decorators";

  2. class Dog {

  3.  @readonly

  4.  bark () {

  5.    return '汪汪汪!!'

  6.  }

  7. }

  8. let dog = new Dog()

  9. dog.bark = 'wangwang!!';

  10. // Cannot assign to read only property 'bark' of [object Object]

  11. // 这里readonly修饰器把Dog类的bark方法修改为只读状态

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

  1. /**

  2. * @param target 目标类Dog

  3. * @param name 所要修饰的属性名 bark

  4. * @param descriptor 该属性的描述对象 bark方法

  5. */

  6. function readonly(target, name, descriptor) {

  7.  // descriptor对象原来的值如下

  8.  // {

  9.  //   value: specifiedFunction,

  10.  //   enumerable: false,

  11.  //   configurable: true,

  12.  //   writable: true

  13.  // };

  14.  descriptor.writable = false;

  15.  return descriptor

  16. }

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

decorator 实用的decorator库 core-decorators.js

npm install core-decorators --save

  1. // 将某个属性或方法标记为不可写。

  2. @readonly  

  3. // 标记一个属性或方法,以便它不能被删除; 也阻止了它通过Object.defineProperty被重新配置

  4. @nonconfigurable  

  5. // 立即将提供的函数和参数应用于该方法,允许您使用lodash提供的任意助手来包装方法。 第一个参数是要应用的函数,所有其他参数将传递给该装饰函数。

  6. @decorate  

  7. // 如果你没有像Babel 6那样的装饰器语言支持,或者甚至没有编译器的vanilla ES5代码,那么可以使用applyDecorators()助手。

  8. @extendDescriptor

  9. // 将属性标记为不可枚举。

  10. @nonenumerable

  11. // 防止属性初始值设定项运行,直到实际查找修饰的属性。

  12. @lazyInitialize

  13. // 强制调用此函数始终将此引用到类实例,即使该函数被传递或将失去其上下文。

  14. @autobind

  15. // 使用弃用消息调用console.warn()。 提供自定义消息以覆盖默认消息。

  16. @deprecate

  17. // 在调用装饰函数时禁止任何JavaScript console.warn()调用。

  18. @suppressWarnings

  19. // 将属性标记为可枚举。

  20. @enumerable

  21. // 检查标记的方法是否确实覆盖了原型链上相同签名的函数。

  22. @override  

  23. // 使用console.time和console.timeEnd为函数计时提供唯一标签,其默认前缀为ClassName.method。

  24. @time

  25. // 使用console.profile和console.profileEnd提供函数分析,并使用默认前缀为ClassName.method的唯一标签。

  26. @profile

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

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

作者:吴鹏和 罗学

  • noConcurrent 避免并发调用,在上一次操作结果返回之前,不响应重复操作

  1. import {noConcurrent} from './decorators';

  2. methods: {

  3.  @noConcurrent     //避免并发,点击提交后,在接口返回之前无视后续点击

  4.  async onSubmit(){

  5.    let submitRes = await this.$http({...});

  6.    //...

  7.    return;

  8.  }

  9. }

  • makeMutex 多函数互斥,具有相同互斥标识的函数不会并发执行

  1. import {makeMutex} from './decorators';

  2. let globalStore = {};

  3. class Navigator {

  4.  @makeMutex({namespace:globalStore, mutexId:'navigate'}) //避免跳转相关函数并发执行

  5.  static async navigateTo(route){...}

  6.  @makeMutex({namespace:globalStore, mutexId:'navigate'}) //避免跳转相关函数并发执行

  7.  static async redirectTo(route){...}

  8. }

  • withErrToast 捕获async函数中的异常,并进行错误提示

  1. methods: {

  2.  @withErrToast({defaultMsg: '网络错误', duration: 2000})

  3.  async pullData(){

  4.    let submitRes = await this.$http({...});

  5.    //...

  6.    return '其他原因'; // toast提示 其他原因

  7.    // return 'ok';   // 正常无提示

  8.  }

  9. }

  • mixinList 用于分页加载,上拉加载时返回拼接数据及是否还有数据提示

  1. methods: {

  2.  @mixinList({needToast: false})

  3.  async loadGoods(params = {}){

  4.    let goodsRes = await this.$http(params);

  5.    return goodsRes.respData.infos;

  6.  },

  7.  async hasMore() {

  8.    let result = await this.loadgoods(params);

  9.    if(result.state === 'nomore') this.tipText = '没有更多了';

  10.    this.goods = result.list;

  11.  }

  12. }

  13. // 上拉加载调用hasMore函数,goods数组就会得到所有拼接数据

  14. // loadGoods可传三个参数 params函数需要参数 ,startNum开始的页码,clearlist清空数组

  15. // mixinList可传一个参数 needToast 没有数据是否需要toast提示

typeCheck 检测函数参数类型

  1. methods: {

  2.  @typeCheck('number')

  3.  btnClick(index){ ... },

  4. }

  5. // btnClick函数的参数index不为number类型 则报错

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

  1. @Buried

  2. methods: {

  3.  btn1Click() {

  4.    // 埋点为 btn1Click

  5.  },

  6.  btn2Click() {

  7.    return 'noBuried'; // 无埋点

  8.  },

  9. },

  10. created() {

  11.  // 埋点为 view

  12. }

  13. // 统计页面展现量和所有methods方法点击量

decorators.js

  1. /**

  2. * 避免并发调用,在上一次操作结果返回之前,不响应重复操作

  3. * 如:用户连续多次点击同一个提交按钮,希望只响应一次,而不是同时提交多份表单

  4. * 说明:

  5. *    同步函数由于js的单线程特性没有并发问题,无需使用此decorator

  6. *    异步时序,为便于区分操作结束时机,此decorator只支持修饰async函数

  7. */

  8. export const noConcurrent = _noConcurrentTplt.bind(null,{mutexStore:'_noConCurrentLocks'});

  9. /**

  10. * 避免并发调用修饰器模板

  11. * @param {Object} namespace 互斥函数间共享的一个全局变量,用于存储并发信息,多函数互斥时需提供;单函数自身免并发无需提供,以本地私有变量实现

  12. * @param {string} mutexStore 在namespace中占据一个变量名用于状态存储

  13. * @param {string} mutexId   互斥标识,具有相同标识的函数不会并发执行,缺省值:函数名

  14. * @param target

  15. * @param funcName

  16. * @param descriptor

  17. * @private

  18. */

  19. function _noConcurrentTplt({namespace={}, mutexStore='_noConCurrentLocks', mutexId}, target, funcName, descriptor) {

  20.  namespace[mutexStore] = namespace[mutexStore] || {};

  21.  mutexId = mutexId || funcName;

  22.  let oriFunc = descriptor.value;

  23.  descriptor.value = function () {

  24.    if (namespace[mutexStore][mutexId]) //上一次操作尚未结束,则无视本次调用

  25.      return;

  26.    namespace[mutexStore][mutexId] = true; //操作开始

  27.    let res = oriFunc.apply(this, arguments);

  28.    if (res instanceof Promise)

  29.      res.then(()=> {

  30.        namespace[mutexStore][mutexId] = false;

  31.      }).catch((e)=> {

  32.        namespace[mutexStore][mutexId] = false;

  33.        console.error(funcName, e);

  34.      }); //操作结束

  35.    else {

  36.      console.error('noConcurrent decorator shall be used with async function, yet got sync usage:', funcName);

  37.      namespace[mutexStore][mutexId] = false;

  38.    }

  39.    return res;

  40.  }

  41. }

  42. /**

  43. * 多函数互斥,具有相同互斥标识的函数不会并发执行

  44. * @param namespace 互斥函数间共享的一个全局变量,用于存储并发信息

  45. * @param mutexId   互斥标识,具有相同标识的函数不会并发执行

  46. * @return {*}

  47. */

  48. export function makeMutex({namespace, mutexId}) {

  49.  if (typeof namespace !== "object") {

  50.    console.error('[makeNoConcurrent] bad parameters, namespace shall be a global object shared by all mutex funcs, got:', namespace);

  51.    return function () {}

  52.  }

  53.  return _noConcurrentTplt.bind(null, {namespace, mutexStore:'_noConCurrentLocksNS', mutexId})

  54. }

  55. /**

  56. * 捕获async函数中的异常,并进行错误提示

  57. * 函数正常结束时应 return 'ok',return其它文案时将toast指定文案,无返回值或产生异常时将toast默认文案

  58. * @param {string} defaultMsg  默认文案

  59. * @param {number, optional} duration 可选,toast持续时长

  60. */

  61. export function withErrToast({defaultMsg, duration=2000}) {

  62.  return function (target, funcName, descriptor) {

  63.    let oriFunc = descriptor.value;

  64.    descriptor.value = async function () {

  65.      let errMsg = '';

  66.      let res = '';

  67.      try {

  68.        res = await oriFunc.apply(this, arguments);

  69.        if (res != 'ok')

  70.          errMsg = typeof res === 'string' ? res : defaultMsg;

  71.      } catch (e) {

  72.        errMsg = defaultMsg;

  73.        console.error('caught err with func:',funcName, e);

  74.      }

  75.      if (errMsg) {

  76.        this.$toast({

  77.          title: errMsg,

  78.          type: 'fail',

  79.          duration: duration,

  80.        });

  81.      }

  82.      return res;

  83.    }

  84.  }

  85. }

  86. /**

  87. * 分页加载

  88. * @param {[Boolean]} [是否加载为空显示toast]

  89. * @return {[Function]} [decrotor]

  90. */

  91. export function mixinList ({needToast = false}) {

  92.  let oldList = [],

  93.      pageNum = 1,

  94.  /**

  95.  * state [string]

  96.  *   hasmore  [还有更多]

  97.  *   nomore   [没有更多了]

  98.  */

  99.  state = 'hasmore',

  100.  current = [];

  101.  return function (target,name,descriptor) {

  102.    const oldFunc  = descriptor.value,

  103.          symbol   = Symbol('freeze');

  104.    target[symbol] = false;

  105.    /**

  106.     * [description]

  107.     * @param  {[Object]}   params={}       [请求参数]

  108.     * @param  {[Number]}   startNum=null   [手动重置加载页数]

  109.     * @param  {[Boolean]}  clearlist=false [是否清空数组]

  110.     * @return {[Object]}   [{所有加载页数组集合,加载完成状态}]

  111.     */

  112.    descriptor.value = async function(params={},startNum=null,clearlist=false) {

  113.      try {

  114.        if (target[symbol]) return;

  115.        // 函数执行前赋值操作

  116.        target[symbol] = true;

  117.        params.data.pageNum = pageNum;

  118.        if (startNum !== null && typeof startNum === 'number') {

  119.          params.data.pageNum = startNum;

  120.          pageNum = startNum;

  121.        }

  122.        if (clearlist) oldList = [];

  123.        // 释放函数,取回list

  124.        let before = current;

  125.        current = await oldFunc.call(this,params);

  126.        // 函数执行结束赋值操作

  127.        (state === 'hasmore' || clearlist) && oldList.push(...current);

  128.        if ((current.length === 0) || (params.data.pageSize > current.length)) {

  129.          needToast && this.$toast({title: '没有更多了',type: 'fail'});

  130.          state = 'nomore';

  131.        } else {

  132.          state = 'hasmore';

  133.          pageNum++;

  134.        }

  135.        target[symbol] = false;

  136.        this.$apply();

  137.        return { list : oldList,state };

  138.      } catch(e) {

  139.        console.error('fail code at: ' + e)

  140.      }

  141.    }

  142.  }

  143. }

  144. /**

  145. * 检测工具

  146. */

  147. const _toString = Object.prototype.toString;

  148. // 检测是否为纯粹的对象

  149. const _isPlainObject = function  (obj) {

  150.  return _toString.call(obj) === '[object Object]'

  151. }

  152. // 检测是否为正则

  153. const _isRegExp = function  (v) {

  154.  return _toString.call(v) === '[object RegExp]'

  155. }

  156. /**

  157. * @description 检测函数

  158. *  用于检测类型action

  159. * @param {Array} checked 被检测数组

  160. * @param {Array} checker 检测数组

  161. * @return {Boolean} 是否通过检测

  162. */

  163. const _check = function (checked,checker) {

  164.  check:

  165.  for(let i = 0; i < checked.length; i++) {

  166.    if(/(any)/ig.test(checker[i]))

  167.      continue check;

  168.    if(_isPlainObject(checked[i]) && /(object)/ig.test(checker[i]))

  169.      continue check;

  170.    if(_isRegExp(checked[i]) && /(regexp)/ig.test(checker[i]))

  171.      continue check;

  172.    if(Array.isArray(checked[i]) && /(array)/ig.test(checker[i]))

  173.      continue check;

  174.    let type = typeof checked[i];

  175.    let checkReg = new RegExp(type,'ig')

  176.    if(!checkReg.test(checker[i])) {

  177.      console.error(checked[i] + 'is not a ' + checker[i]);

  178.      return false;

  179.    }

  180.  }

  181.  return true;

  182. }

  183. /**

  184. * @description 检测类型

  185. *   1.用于校检函数参数的类型,如果类型错误,会打印错误并不再执行该函数;

  186. *   2.类型检测忽略大小写,如string和String都可以识别为字符串类型;

  187. *   3.增加any类型,表示任何类型均可检测通过;

  188. *   4.可检测多个类型,如 "number array",两者均可检测通过。正则检测忽略连接符 ;

  189. */

  190. export function typeCheck() {

  191.  const checker =  Array.prototype.slice.apply(arguments);

  192.  return function (target, funcName, descriptor) {

  193.    let oriFunc = descriptor.value;

  194.    descriptor.value =  function () {

  195.      let checked =  Array.prototype.slice.apply(arguments);

  196.      let result = undefined;

  197.      if(_check(checked,checker) ){

  198.        result = oriFunc.call(this,...arguments);

  199.      }

  200.      return result;

  201.    }

  202.  }

  203. };

  204. const errorLog = (text) => {

  205.  console.error(text);

  206.  return true;

  207. }

  208. /**

  209. * @description 全埋点

  210. *  1.在所有methods方法中埋点为函数名

  211. *  2.在钩子函数中'beforeCreate','created','beforeMount','mounted','beforeUpdate','activated','deactivated'依次寻找这些钩子

  212. *    如果存在就会增加埋点 VIEW

  213. *

  214. * 用法:

  215. *   @Buried

  216. *   在单文件导出对象一级子对象下;

  217. *   如果某方法不想设置埋点 可以 return 'noBuried' 即可

  218. */

  219. export function Buried(target, funcName, descriptor) {

  220.  let oriMethods = Object.assign({},target.methods),

  221.      oriTarget = Object.assign({},target);

  222.  // methods方法中

  223.  if(target.methods) {

  224.    for(let name in target.methods) {

  225.      target.methods[name] = function () {

  226.        let result = oriMethods[name].call(this,...arguments);

  227.        // 如果方法中返回 noBuried 则不添加埋点

  228.        if(typeof result === 'string' && result.includes('noBuried')) {

  229.          console.log(name + '方法设置不添加埋点');

  230.        } else if(result instanceof Promise) {

  231.          result.then(res => {

  232.            if(typeof res === 'string' && res.includes('noBuried')) { console.log(name + '方法设置不添加埋点'); return; };

  233.            console.log('添加埋点在methods方法中:' , name.toUpperCase ());

  234.            this.$log(name);

  235.          });

  236.        }else{

  237.          console.log('添加埋点在methods方法中:' , name.toUpperCase ());

  238.          this.$log(name);

  239.        };

  240.        return result;

  241.      }

  242.    }

  243.  }

  244.  // 钩子函数中

  245.  const hookFun = (hookName) => {

  246.    target[hookName] = function() {

  247.      let result =  oriTarget[hookName].call(this,...arguments);

  248.      console.log('添加埋点,在钩子函数' + hookName + '中:', 'VIEW');

  249.      this.$log('VIEW');

  250.      return result;

  251.    }

  252.  }

  253.  const LIFECYCLE_HOOKS = [

  254.    'beforeCreate',

  255.    'created',

  256.    'beforeMount',

  257.    'mounted',

  258.    'beforeUpdate',

  259.    'activated',

  260.    'deactivated',

  261.  ];

  262.  for(let item of LIFECYCLE_HOOKS) {

  263.    if (target[item]) return hookFun(item);

  264.  }

  265. }