標籤:

es7你都懂了嗎?今天帶你了解es7的神器decorator

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

還有很多這裡就不過多介紹,了解更多 github.com/jayphelps/co

下面給大家介紹一些我們團隊寫的一些很實用的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 即可

@Buriedmethods: { 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);}}

推薦閱讀:

TAG:裝飾器 |