一文讀懂@Decorator裝飾器——理解VS Code源碼的基礎(chǔ)(下)

導(dǎo)語 | 本人在讀VS Code源碼的時(shí)候,發(fā)現(xiàn)其用了大量的@Decorator裝飾器語法,由于對(duì)裝飾器的語法比較陌生,它成為了我理解VS Code的攔路虎。其實(shí)不止VS Code,Angular、Node.js框架Nest.js、TypeORM、Mobx(5) 和Theia等都深度用到了裝飾器語法,為了讀懂各大優(yōu)秀開源項(xiàng)目,讓我們先一起來把@Decorator裝飾器的原理以及用法徹底弄懂。
一、@Decorator裝飾器語法糖
《一文讀懂@Decorator裝飾器——理解VS Code源碼的基礎(chǔ)(上)》中手寫的裝飾器函數(shù)存在兩個(gè)可優(yōu)化的點(diǎn):
是否可以讓裝飾器函數(shù)更關(guān)注業(yè)務(wù)邏輯?
《一文讀懂@Decorator裝飾器——理解VS Code源碼的基礎(chǔ)(上)》中Step1, Step2是通用邏輯的,每個(gè)裝飾器都需要實(shí)現(xiàn),簡單來說就是可復(fù)用的。
是否可以讓裝飾器寫法更簡單?
純函數(shù)實(shí)現(xiàn)的裝飾器,每裝飾一個(gè)屬性都要手動(dòng)執(zhí)行裝飾器函數(shù),詳細(xì)內(nèi)容見《一文讀懂@Decorator裝飾器——理解VS Code源碼的基礎(chǔ)(上)》中Step4步驟。
針對(duì)上述優(yōu)化點(diǎn),裝飾器草案中有一顆特別甜的語法糖,也就是@Decorator,它能夠幫你省去很多繁瑣的步驟來用上裝飾器。只需要在想使用的裝飾器前加上@符號(hào),裝飾器就會(huì)被應(yīng)用到目標(biāo)上。
(一)@Decorator語法糖的便捷性
下面我們用@Decorator的寫法,來實(shí)現(xiàn)同樣的功能,看看代碼量可以精簡多少:
// Step2 編寫裝飾器函數(shù)業(yè)務(wù)邏輯代碼function logTime(target, key, descriptor) {const oldMethed = descriptor.valueconst logTime = function (...arg) {let start = +new Date()try {return oldMethed.apply(this, arg) // 調(diào)用之前的函數(shù)} finally {let end = +new Date()console.log(`耗時(shí): ${end - start}ms`)}}descriptor.value = logTimereturn descriptor}class GuanYu {// Step4 利用 @ 語法糖裝飾指定屬性@logTimeattack() {console.log('揮了一次大刀')}// Step4 利用 @ 語法糖裝飾指定屬性@logTimerun() {console.log('跑了一段距離')}}const guanYu = new GuanYu()guanYu.attack()// [LOG]: 揮了一次大刀// [LOG]: 耗時(shí): 3msguanYu.run()// [LOG]: 跑了一段距離// [LOG]: 耗時(shí): 3ms
為了讓大家更直觀了解上述代碼是否可以編譯后正常執(zhí)行,我們可以從TypeScript Playground(https://www.typescriptlang.org/play) 直接看到編譯后的代碼以及運(yùn)行結(jié)果。
注意!為了方便理解,記得關(guān)閉配置emitDecoratorMetadata禁止輸出元數(shù)據(jù),元數(shù)據(jù)是另一個(gè)比較復(fù)雜的知識(shí)點(diǎn),我們本篇文章先跳過。關(guān)閉后編譯的代碼會(huì)更簡單!
我們打開上面代碼的在線Playground鏈接,點(diǎn)擊Run運(yùn)行按鈕,即可看到其正常運(yùn)行和輸出結(jié)果:

對(duì)比純手寫的裝飾器,用@Decorator語法糖可以省去2個(gè)重復(fù)的步驟:
Step1 備份原來類構(gòu)造器(Class.prototype)的屬性描述符(Descriptor)
const oldDescriptor = Object.getOwnPropertyDescriptor(targetPrototype, key)Step3 用裝飾器函數(shù)覆蓋原來屬性描述符的value
Object.defineProperty(targetPrototype, key, {...oldDescriptor,value: logTime})
開發(fā)者僅需兩步即可實(shí)現(xiàn)裝飾器的功能,可以更專注于裝飾器本身的業(yè)務(wù)邏輯:
Step2 編寫裝飾器函數(shù)業(yè)務(wù)邏輯代碼
function logTime(target, key, descriptor) {const oldMethed = descriptor.valueconst logTime = function (...arg) {let start = +new Date()try {return oldMethed.apply(this, arg) // 調(diào)用之前的函數(shù)} finally {let end = +new Date()console.log(`耗時(shí): ${end - start}ms`)}}descriptor.value = logTimereturn descriptor}
Step4 利用@語法糖裝飾指定屬性
attack() {console.log('揮了一次大刀')}
(二)分析@Decorator語法糖編譯后的代碼【重點(diǎn)】
@Decorator語法糖很甜,但卻不能直接食用。因?yàn)檠b飾器目前僅僅是ECMAScript的語言提案,還處于stage-2階段。
(https://github.com/tc39/proposal-decorators)
無論是最新版的Chrome瀏覽器還是Node.js都不能直接運(yùn)行帶有@Decorator語法糖的代碼。
我們需要借助TypeScript或者Babel的能力,將源碼編譯后才能正常運(yùn)行。而在TypeSciprt Playground上,我們可以直接看到編譯后代碼。
為了更清晰容易理解,我們把編譯后的業(yè)務(wù)代碼先注釋掉,只看裝飾器實(shí)現(xiàn)的相關(guān)代碼:
;// Part1 裝飾器工具函數(shù)(__decorate)的定義var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;return c > 3 && r && Object.defineProperty(target, key, r), r;};function logTime(target, key, descriptor) {// ...}class GuanYu {// ...}// Part2 裝飾器工具函數(shù)(__decorate)的執(zhí)行__decorate([logTime], GuanYu.prototype, "attack", null);__decorate([logTime], GuanYu.prototype, "run", null);// ...
上述代碼核心點(diǎn)是兩個(gè)部分,一個(gè)是定義,一個(gè)是執(zhí)行。定義部分較為復(fù)雜,我們先從執(zhí)行入手:
Part2 裝飾器工具函數(shù)(__decorate)的執(zhí)行會(huì)傳入以下4個(gè)參數(shù):
裝飾器業(yè)務(wù)邏輯函數(shù)
類的構(gòu)造器
類的構(gòu)造器屬性名
屬性描述符(可以為null)
為了方便理解Part1裝飾器工具函數(shù)__decorate的定義,我們需要來精簡__decorate的函數(shù)代碼,讓它變成最簡單的樣子,而精簡代碼的前提是收集條件:
條件1 (this&&this.__decorate) 可刪除
這里的this是指window對(duì)象,這一步的含義是避免重復(fù)定義__decorate函數(shù),屬于輔助代碼,可刪掉。
條件2 c<3===false
Part1的c=arguments.length代表參數(shù)的個(gè)數(shù),由Part2我們知道工具函數(shù)會(huì)傳入4個(gè)參數(shù),因此在本次案例中c<3參數(shù)個(gè)數(shù)小于3的情況不存在,即c<3===false。
條件3 c>3===true
本次案例中c>3參數(shù)大于3的情況存在,即c>3===true。
條件4 desc===null
同時(shí)在Part1我們知道第四個(gè)參數(shù)desc傳入的值就是null,即desc===null。
條件5 typeof Reflect!=="object"
Reflect反射是ES6的語法,本文為了更容易理解,暫不引入新的ES6特性和語法,讓環(huán)境默認(rèn)為ES5,即不存在Reflect對(duì)象,即typeof Reflect!=="object"。
有了上述條件后,我們可以進(jìn)一步精簡__decorate的方法:
代碼片段1:
r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc// 根據(jù) c < 3 === false , desc === null 條件// 精簡后r = desc = Object.getOwnPropertyDescriptor(target, key)// r 和 desc 此時(shí)代表的是屬性的描述符 Descriptor
代碼片段2:
if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;// 根據(jù) c < 3 === false , c > 3 === true 條件// 精簡后if (d = decorators[i]) r = d(target, key, r) || r;
代碼片段3:
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);// 為了方便理解,本案例暫認(rèn)為 Reflect 不存在// 精簡后// 空
代碼片段4:
return c > 3 && r && Object.defineProperty(target, key, r), r;// 根據(jù) c > 3 === true, r 是屬性描述符,必定存在// 精簡后Object.defineProperty(target, key, r)return r;
精簡后整體代碼:
var __decorate = function (decorators, target, key, desc) {var c = arguments.length;// Step1 備份原來類構(gòu)造器 (Class.prototype) 的屬性描述符 (Descriptor)var r = desc = Object.getOwnPropertyDescriptor(target, key),var d;for (var i = decorators.length - 1; i >= 0; i--) {// d 為裝飾器業(yè)務(wù)邏輯函數(shù)if (d = decorators[i]) {// 執(zhí)行 d,并傳入 target 類構(gòu)造器,key 屬性名,r 屬性描述符r = d(target, key, r) || r;}}// Step3 用裝飾器函數(shù)覆蓋原來屬性描述符Object.defineProperty(target, key, r)return r;};
代碼經(jīng)過精簡之后核心原理還是和我們《一文讀懂@Decorator裝飾器——理解VS Code源碼的基礎(chǔ)(上)》中手寫一個(gè)裝飾器函數(shù)的原理是一樣的。
Step1 備份原來類構(gòu)造器 (Class.prototype) 的屬性描述符(Descriptor)
利用Object.getOwnPropertyDescriptor獲取
Step3 用裝飾器函數(shù)覆蓋原來屬性描述符的value
利用Object.defineProperty代理
TypeScript對(duì)裝飾器編譯后的代碼,只不過是把裝飾器可復(fù)用的邏輯抽離成一個(gè)工具函數(shù),方便復(fù)用而已。
分析到這里,是不是對(duì)@Decorator裝飾器最根本的實(shí)現(xiàn)有了更深入的了解?
從上面的例子,我們也進(jìn)一步驗(yàn)證了:
Decorator Pattern裝飾器模式的設(shè)計(jì)理念:在不修改原有代碼情況下,對(duì)功能進(jìn)行擴(kuò)展。
Decorator裝飾器的具體實(shí)現(xiàn),本質(zhì)是函數(shù),參數(shù)有target, key, descriptor。
@Decoretor是裝飾器的一種語法糖,只是一種便捷寫法,編譯后本質(zhì)還是一個(gè)函數(shù)。
二、帶參數(shù)的裝飾器:裝飾器工廠函數(shù)
在《一文讀懂@Decorator裝飾器——理解VS Code源碼的基礎(chǔ)(上)》的「記錄函數(shù)耗時(shí)」例子中,如果我們希望在日志前面加個(gè)可變的標(biāo)簽,如何實(shí)現(xiàn)?
答案是使用帶參數(shù)的裝飾器。
重點(diǎn):logTime是個(gè)高階函數(shù),可以理解成裝飾器工廠函數(shù),其接收參數(shù)執(zhí)行后,返回一個(gè)裝飾器函數(shù)。
function logTime(tag) { // 這是一個(gè)裝飾器工廠函數(shù)return function(target, key, descriptor) { // 這是裝飾器const oldMethed = descriptor.valueconst logTime = function (...arg) {let start = +new Date()try {return oldMethed.apply(this, arg)} finally {let end = +new Date()console.log(`【${tag}】耗時(shí): ${end - start}ms`)}}descriptor.value = logTimereturn descriptor}}class GuanYu {@logTime('攻擊')attack() {console.log('揮了一次大刀')},@logTime('奔跑')run() {console.log('跑了一段距離')}}// ...
編譯后:
// ...__decorate([logTime('攻擊')], GuanYu.prototype, "attack", null);__decorate([logTime('奔跑')], GuanYu.prototype, "run", null);// ...
看了編譯后的代碼,我們就很容易知道帶參數(shù)裝飾器的具體實(shí)現(xiàn)原理,無非是直接先執(zhí)行裝飾器工廠函數(shù),此時(shí)傳入對(duì)應(yīng)參數(shù),然后返回一個(gè)新的裝飾器業(yè)務(wù)邏輯的函數(shù)。
三、五種裝飾器
我們前面學(xué)了那么多裝飾器的內(nèi)容,其實(shí)只學(xué)了一種裝飾器:方法裝飾器。
而裝飾器一共有5種類型可被我們使用:
類裝飾器
屬性裝飾器
方法裝飾器
訪問器裝飾器
參數(shù)裝飾器
先來個(gè)全家福,然后我們逐一攻破:
// 類裝飾器class GuanYu {// 屬性裝飾器name: string;// 方法裝飾器attack (// 參數(shù)裝飾器meters: number) {}// 訪問器裝飾器get horse() {}}
(一)類裝飾器
類型聲明:
// 類裝飾器function classDecorator(target: any) {return // ...};
@參數(shù):只接受一個(gè)參數(shù)
@返回:如果類裝飾器返回了一個(gè)值,她將會(huì)被用來代替原有的類構(gòu)造器的聲明
因此,類裝飾器適合用于繼承一個(gè)現(xiàn)有類并添加一些屬性和方法。
例如我們可以添加一個(gè)addToJsonString方法給所有的類來新增一個(gè)toString方法:
function addToJSONString(target) {return class extends target {toJSONString() {return JSON.stringify(this);}};}@addToJSONStringclass C {public foo = "foo";public num = 24;}console.log(new C().toJSONString())// [LOG]: "{"foo":"foo","num":24}"
(二)方法裝飾器
已經(jīng)在前面章節(jié)介紹過利用方法裝飾器來實(shí)現(xiàn)「記錄函數(shù)耗時(shí)」功能,現(xiàn)在我們重新復(fù)習(xí)下。
類型聲明:
// 方法裝飾器function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {return // ...};
@參數(shù):
target: 對(duì)于靜態(tài)成員來說是類的構(gòu)造器,對(duì)于實(shí)例成員來說是類的原型鏈。
propertyKey: 屬性的名稱。
descriptor: 屬性的描述器。
(https://developer.mozilla.org/zh- CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor)
@返回:如果返回了值,它會(huì)被用于替代屬性的描述器。
利用方法裝飾器我們可以實(shí)現(xiàn)更多的具體場(chǎng)景,比如「打印Request的請(qǐng)求參數(shù)和結(jié)果」功能:
function loggerParamsResult(target, propertyKey, descriptor) {const original = descriptor.value;descriptor.value = async function (...args) {let resultlet errortry {result = await original.call(this, ...args);} catch(e) {error = new Error(e)}if (error) {console.error('請(qǐng)求失?。?)console.error('請(qǐng)求參數(shù): ', ...args)console.error('失敗原因: ', error)} else {console.log('請(qǐng)求成功!')console.log('請(qǐng)求參數(shù)', ...args)console.log('請(qǐng)求結(jié)果: ', result)}return result;}}class App {@loggerParamsResultrequest(data) {return new Promise((resolve, reject) => {const random = Math.random() > 0.5if (random) {resolve(random)} else {reject(random)}})}}const app = new App();app.request({ url: 'https://www.tencent.com'});// [LOG]: "請(qǐng)求成功!"// [LOG]: "請(qǐng)求參數(shù)", {// "url": "https://www.tencent.com"// }// [LOG]: "請(qǐng)求結(jié)果: ", true// [ERR]: "請(qǐng)求失??!"// [ERR]: "請(qǐng)求參數(shù): ", {// "url": "https://www.tencent.com"// }// [ERR]: "失敗原因: ", false
總結(jié):無論是「記錄函數(shù)耗時(shí)」還是「打印Request的請(qǐng)求參數(shù)和結(jié)果」,本質(zhì)都是在實(shí)現(xiàn)Before/After鉤子,因此我們只需要記住方法裝飾器可以實(shí)現(xiàn)與Before/After鉤子相關(guān)的場(chǎng)景功能。
(三)屬性裝飾器
類型聲明:
// 屬性裝飾器function propertyDecorator(target: any, propertyKey: string) {}
@參數(shù): 只接受兩個(gè)參數(shù),少了descriptor描述器
target: 對(duì)于靜態(tài)成員來說是類的構(gòu)造器,對(duì)于實(shí)例成員來說是類的原型鏈。
propertyKey: 屬性的名稱。
@返回: 返回的結(jié)果將被忽略
利用屬性裝飾器,我們可以實(shí)現(xiàn)一個(gè)非常簡單的屬性監(jiān)聽功能,當(dāng)屬性改變時(shí)觸發(fā)指定函數(shù):
function observable(fnName) { // 裝飾器工廠函數(shù)return function (target: any, key: string): any { // 裝飾器let prev = target[key];Object.defineProperty(target, key, {set(next) {target[fnName](prev, next);prev = next;}})}}class Store {('onCountChange')count = -1;onCountChange(prev, next) {console.log('>>> count has changed!')console.log('>>> prev: ', prev)console.log('>>> next: ', next)}}const store = new Store();store.count = 10// [LOG]: ">>> count has changed!"// [LOG]: ">>> prev: ", undefined// [LOG]: ">>> next: ", -1// [LOG]: ">>> count has changed!"// [LOG]: ">>> prev: ", -1// [LOG]: ">>> next: ", 10
(四)訪問器裝飾器
訪問器裝飾器總體上講和方法裝飾器很接近,唯一的區(qū)別在于第三個(gè)參數(shù)descriptor描述器中有的key不同:
方法裝飾器的描述器的key為:
value
writable
enumerable
configurable
訪問器裝飾器的描述器的key為:
get
set
enumerable
configurable
類型聲明:
// 訪問器裝飾器function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {return // ...};
例如,我們可以將某個(gè)屬性在賦值的時(shí)候做一層代理,額外相加一個(gè)值:
function addExtraNumber(num) { // 裝飾器工廠函數(shù)return function (target, propertyKey, descriptor) { // 裝飾器const original = descriptor.set;descriptor.set = function (value) {const newObj = {}Object.keys(value).forEach(key => {newObj[key] = value[key] + num})return original.call(this, newObj)}}}class C {private _point = { x: 0, y: 0 }(2)set point(value: { x: number, y: number }) {this._point = value;}get point() {return this._point;}}const c = new C();c.point = { x: 1, y: 1 };console.log(c.point)// [LOG]: {// "x": 3,// "y": 3// }
(五)參數(shù)裝飾器
類型聲明:
// 參數(shù)裝飾器function parameterDecorator(target: any, methodKey: string, parameterIndex: number) {}
@參數(shù):接收三個(gè)參數(shù)
target: 對(duì)于靜態(tài)成員來說是類的構(gòu)造器,對(duì)于實(shí)例成員來說是類的原型鏈。
methodKey: 方法的名稱,注意!是方法的名稱,而不是參數(shù)的名稱。
parameterIndex: 參數(shù)在方法中所處的位置的下標(biāo)。
@返回:返回的值將會(huì)被忽略
單獨(dú)的參數(shù)裝飾器能做的事情很有限,它一般都被用于記錄可被其它裝飾器使用的信息。
function Log(target, methedKey, parameterIndex) {console.log(`方法名稱 ${methedKey}`);console.log(`參數(shù)順序 ${parameterIndex}`);}class GuanYu {attack(@Log person, @Log dog) {console.log(`向 ${person} 揮了一次大刀`)}}// [LOG]: "方法名稱 attack"// [LOG]: "參數(shù)順序 0"
(六)裝飾器參數(shù)總結(jié)

四、裝飾器順序
(一)同種裝飾器組合順序:洋蔥模型
如果同一個(gè)方法有多個(gè)裝飾器,其執(zhí)行順序是怎樣的?
答案:以方法裝飾器為例,同種裝飾器組合后,其順序會(huì)像剝洋蔥一樣,先從外到內(nèi)進(jìn)入,然后由內(nèi)向外執(zhí)行。和Koa的中間件順序類似。
function dec(id){console.log('裝飾器初始化', id);return function (target, property, descriptor) {console.log('裝飾器執(zhí)行', id);}}class Example {@dec(1)@dec(2)method(){}}// 裝飾器初始化 1// 裝飾器初始化 2// 裝飾器執(zhí)行 2// 裝飾器執(zhí)行 1

其原理,看編譯后的代碼就非常清楚:
重點(diǎn):
dec(1), dec(2)初始化時(shí)就執(zhí)行。
for(var i=decorators.length-1; i>=0;i--) 是從右向左,倒敘執(zhí)行。
// 由于本段代碼不存在 c < 3 (參數(shù)少于3個(gè)) 的情況,為了方便理解已精簡了部分不可能執(zhí)行的代碼var __decorate = function (decorators, target, key, desc) {var c = arguments.length,r = desc = Object.getOwnPropertyDescriptor(target, key),d;for (var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = d(target, key, r) || r;Object.defineProperty(target, key, r)return r;};function dec(id) {console.log('裝飾器初始化', id);return function (target, property, descriptor) {console.log('裝飾器執(zhí)行', id);};}class Example {method() { }}__decorate([dec(1),dec(2)], Example.prototype, "method", null);// 裝飾器初始化 1// 裝飾器初始化 2// 裝飾器執(zhí)行 2// 裝飾器執(zhí)行 1
(二)不同類型裝飾器順序:有規(guī)則有規(guī)律
實(shí)例成員:(參數(shù)>方法) 訪問器 屬性 裝飾器 (按順序)
靜態(tài)成員:(參數(shù)>方法) 訪問器 屬性 裝飾器 (按順序)
構(gòu)造器:參數(shù)裝飾器
類裝飾器
多種裝飾器優(yōu)先級(jí)為:實(shí)例成員最高,內(nèi)部成員里面的裝飾器則按定義順序執(zhí)行,依次排下來,類裝飾器最低。
function f(key: string): any {// console.log("初始化: ", key);return function () {console.log("執(zhí)行: ", key);};}("8. 類")class C {("4. 靜態(tài)屬性")static prop?: number;("5. 靜態(tài)方法")static method(("6. 靜態(tài)方法參數(shù)") foo) {}constructor(("7. 構(gòu)造器參數(shù)") foo) {}("2. 實(shí)例方法")method(("1. 實(shí)例方法參數(shù)") foo) {}("3. 實(shí)例屬性")prop?: number;}// "執(zhí)行: ", "1. 實(shí)例方法參數(shù)"// "執(zhí)行: ", "2. 實(shí)例方法"// "執(zhí)行: ", "3. 實(shí)例屬性"// "執(zhí)行: ", "4. 靜態(tài)屬性"// "執(zhí)行: ", "6. 靜態(tài)方法參數(shù)"// "執(zhí)行: ", "5. 靜態(tài)方法"// "執(zhí)行: ", "7. 構(gòu)造器參數(shù)"// "執(zhí)行: ", "8. 類"
五、裝飾器總結(jié)
(一)應(yīng)用場(chǎng)景
裝飾器很像是組合一系列函數(shù),類似于高階函數(shù)和類。合理利用裝飾器對(duì)一些非內(nèi)部邏輯相關(guān)的代碼進(jìn)行封裝提煉,能夠幫助我們快速完成重復(fù)性的工作,節(jié)省時(shí)間,極大提高開發(fā)效率。
類裝飾器
可添加額外的方法和屬性,比如:擴(kuò)展toJSONString方法。
方法裝飾器
可實(shí)現(xiàn)Before/After鉤子功能,比如:記錄函數(shù)耗時(shí),打印request參數(shù)結(jié)果,節(jié)流防抖。
屬性裝飾器
可監(jiān)聽屬性改變觸發(fā)其他事件,比如:實(shí)現(xiàn)count監(jiān)聽器。
訪問器裝飾器
參數(shù)裝飾器
當(dāng)然,還有更多可以使用裝飾器的場(chǎng)景等著我們?nèi)グl(fā)現(xiàn):
運(yùn)行時(shí)類型檢查
依賴注入
(二)優(yōu)點(diǎn)
在不修改原有代碼情況下,對(duì)功能進(jìn)行擴(kuò)展。也就是對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉。
優(yōu)雅地把“輔助性功能邏輯”從“業(yè)務(wù)邏輯”中分離,解耦出來。(AOP 面向切面編程的設(shè)計(jì)理念)
裝飾類和被裝飾類可以獨(dú)立發(fā)展,不會(huì)相互耦合。
裝飾模式是Class繼承的一個(gè)替代模式,可以理解成組合。
(三)缺點(diǎn)
但是糖再好吃,也不要吃太多,容易壞牙齒的,濫用過多裝飾器會(huì)導(dǎo)致很多問題:
理解成本:過多帶業(yè)務(wù)功能的裝飾器會(huì)使代碼本身邏輯變得撲朔迷離。
調(diào)試成本:裝飾器層次增多,會(huì)增加調(diào)試成本,很難追溯到一個(gè)Bug是在哪一層包裝導(dǎo)致的。
(四)注意事項(xiàng)
裝飾器的功能邏輯代碼一定是輔助性的。
比如日志記錄,性能統(tǒng)計(jì)等,這樣才符合AOP面向切面編程的思想,如果把過多的業(yè)務(wù)邏輯寫在了裝飾器上,效果會(huì)適得其反。
裝飾器語法尚未定案以及未被納入ES標(biāo)準(zhǔn),標(biāo)準(zhǔn)化的過程還需要很長時(shí)間。
由于裝飾器語法未來制定的標(biāo)準(zhǔn)可能與當(dāng)前的裝飾器實(shí)現(xiàn)方案有所不同,Mobx6出于兼容性的考慮,放棄了裝飾器的用法,并建議使用makeObservable/makeAutoObservable來進(jìn)行代替。
(https://zh.mobx.js.org/observable-state.html)
詳情請(qǐng)查看:https://zh.mobx.js.org/enabling-decorators.html
裝飾器提案進(jìn)度:https://github.com/tc39/proposal-decorators
作者簡介
阮易強(qiáng)(easonruan)
騰訊高級(jí)前端開發(fā)工程師
騰訊高級(jí)前端開發(fā)工程師,曾負(fù)責(zé)過「粵省事」、「穗康」等大型小程序項(xiàng)目,目前是WeDa微搭低代碼平臺(tái)(專有版)的核心開發(fā)人員,有豐富低代碼平臺(tái)研發(fā)經(jīng)驗(yàn)。
推薦閱讀
一文讀懂@Decorator裝飾器——理解VS Code源碼的基礎(chǔ)(上)
如何用函數(shù)式編程思想優(yōu)化業(yè)務(wù)代碼,這就給你安排上!
拒絕代碼臃腫,這套計(jì)算引擎設(shè)計(jì)方法值得一看!


