es7帶來了很多更強大的方法,比如async/await,decorator等,相信大家對於async/await已經用的很熟練了,下麵我們來講一下decorator。 何為decorator? 官方說法,修飾器(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
還有很多這裡就不過多介紹,瞭解更多 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);
-
}
-
}