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

導語 | 本人在讀VS Code源碼的時候,發(fā)現(xiàn)其用了大量的@Decorator裝飾器語法,由于對裝飾器的語法比較陌生,它成為了我理解VS Code的攔路虎。其實不止VS Code,Angular、Node.js框架Nest.js、TypeORM、Mobx(5) 和Theia等都深度用到了裝飾器語法,為了讀懂各大優(yōu)秀開源項目,讓我們先一起來把@Decorator裝飾器的原理以及用法徹底弄懂。
一、裝飾器的樣子
我們先來看看Decorator裝飾器長什么樣子,大家可能沒在項目中用過Decorator裝飾器,但多多少少會看過下面裝飾器的寫法:
/* Nest.Js cats.controller.ts */import { Controller, Get } from '@nestjs/common';export class CatsController {findAll(): string {return 'This action returns all cats';}}
摘自《Nest.Js》官方文檔(網(wǎng)址:https://docs.nestjs.cn/8/controllers)
上述代碼大家可以不著急去理解,主要是讓大家對裝飾器有一個初步了解,后面我們會逐一分析Decorator裝飾器的實現(xiàn)原理以及具體用法。
二、為什么要理解裝飾器
(一)淺一點來說,理解才能讀懂VS Code源碼
Decorator裝飾器是ECMAScript的語言提案,目前還處于stage-2階段(https://github.com/tc39/proposal-decorators),但是借助TypeScript或者Babel,已經(jīng)有大量的優(yōu)秀開源項目深度用上它了,比如:VS Code, Angular,Nest.Js(后端Node.js框架),TypeORM,Mobx(5) 等等。
舉個例子:
https://github.com/microsoft/vscode/blob/main/src/vs/workbench/services/editor/browser/codeEditorService.ts#L22

作為一個有追求的程序員,你可能會問:上面代碼的裝飾器代表什么含義?去掉裝飾器后能不能正常運行?
如果沒弄懂裝飾器,很難讀懂VS Code這些優(yōu)秀項目源碼的核心思想。所以說你不需要熟練使用裝飾器,但一定要理解裝飾器的用法。
(二)深一點來說,理解才能弄懂AOP,IoC,DI等優(yōu)秀編程思想
AOP即面向切面編程 (Aspect Oriented Programming)
AOP主要意圖是將日志記錄,性能統(tǒng)計,安全控制,異常處理等代碼從業(yè)務(wù)邏輯代碼中劃分出來,將它們獨立到非指導業(yè)務(wù)邏輯的方法中,進而改變這些行為的時候不影響業(yè)務(wù)邏輯的代碼。
簡而言之,就是“優(yōu)雅”地把“輔助功能邏輯”從“業(yè)務(wù)邏輯”中分離,解耦出來。

圖摘自《簡談前端開發(fā)中的AOP(一) -- 前端AOP的實現(xiàn)思路》
(https://zhuanlan.zhihu.com/p/269504590)
IoC即控制反轉(zhuǎn) (Inversion of Control),是解耦的一種設(shè)計理念
DI即依賴注入 (Dependency Injection),是IoC的一種具體實現(xiàn)
使用IoC前:

使用IoC后:

圖摘自《兩張圖讓你理解IoC(控制反轉(zhuǎn))》
(https://learnku.com/laravel/t/3845/the-two-picture-lets-you-understand-ioc-inversion-of-control)
IoC控制反轉(zhuǎn)的設(shè)計模式可以大幅度地降低了程序的耦合性。而Decorator裝飾器在VS Code的控制反轉(zhuǎn)設(shè)計模式里,其主要作用是實現(xiàn)DI依賴注入的功能和精簡部分重復的寫法。
由于該步驟實現(xiàn)較為復雜,我們先從簡單的例子為切入點去了解裝飾器的基本原理。
三、裝飾器的概念區(qū)分
在理解裝飾器之前,有必要先對裝飾器的3個概念進行區(qū)分。
(一)Decorator Pattern(裝飾器模式)
是一種抽象的設(shè)計理念,核心思想是在不修改原有代碼情況下,對功能進行擴展。
(二)Decorator(裝飾器)
是一種特殊的裝飾類函數(shù),是一種對裝飾器模式理念的具體實現(xiàn)。
(三)@Decorator(裝飾器語法)
是一種便捷的語法糖(寫法),通過@來引用,需要編譯后才能運行。理解了概念之后可以知道:裝飾器的存在就是希望實現(xiàn)裝飾器模式的設(shè)計理念。
說法1:在不修改原有代碼情況下,對功能進行擴展。也就是對擴展開放,對修改關(guān)閉。
說法2:優(yōu)雅地把“輔助性功能邏輯”從“業(yè)務(wù)邏輯”中分離,解耦出來。(AOP面向切面編程的設(shè)計理念)
四、裝飾器的實戰(zhàn):記錄函數(shù)耗時
現(xiàn)在有一個關(guān)羽(GuanYu)類,它有兩個函數(shù)方法:attack(攻擊)和run(奔跑):
class GuanYu {attack() {console.log('揮了一次大刀')}run() {console.log('跑了一段距離')}}
而我們都是優(yōu)秀的程序員,時時刻刻都有著經(jīng)營思維(性能優(yōu)化),因此想給關(guān)羽(GuanYu)的函數(shù)方法提前做好準備:
記錄關(guān)羽的每一次attack(攻擊)和run(奔跑)的執(zhí)行時間,以便于后期做性能優(yōu)化。
(一)復制粘貼,不用思考一把梭就是干
拿到需求,不用多想,立刻在函數(shù)前后,添加記錄函數(shù)耗時的邏輯代碼,并復制粘貼到其他地方:
class GuanYu {attack() {+ const start = +new Date()console.log('揮了一次大刀')+ const end = +new Date()+ console.log(`耗時: ${end - start}ms`)}run() {+ const start = +new Date()console.log('跑了一段距離')+ const end = +new Date()+ console.log(`耗時: ${end - start}ms`)}}
但是這樣直接修改原函數(shù)代碼有以下幾個問題:
理解成本高
統(tǒng)計耗時的相關(guān)代碼與函數(shù)本身邏輯并無關(guān)系,對函數(shù)結(jié)構(gòu)造成了破壞性的修改,影響到了對原函數(shù)本身的理解。
維護成本高
如果后期還有更多類似的函數(shù)需要添加統(tǒng)計耗時的代碼,在每個函數(shù)中都添加這樣的代碼非常低效,也大大提高了維護成本。
(二)裝飾器模式,不修改原代碼擴展功能
裝飾器前置基礎(chǔ)知識
在開始用裝飾器實現(xiàn)之前必須掌握以下基礎(chǔ):
Object.getOwnPropertyDescriptor()(https://developer.mozilla.org/zhCN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor)
返回指定對象上一個自有屬性對應(yīng)的屬性描述符:
var a = { b: () => {} }var descriptor = Object.getOwnPropertyDescriptor(a, 'b')console.log(descriptor)/*** {* configurable: true, // 可配置的* enumerable: true, // 可枚舉的* value: () => {}, // 該屬性對應(yīng)的值(數(shù)值,對象,函數(shù)等)* writable: true, // 可寫入的* }*/
這里要注意一個點是:value可以是JavaScript的任意值,比如函數(shù)方法,正則,日期等。
Object.defineProperty()(https://developer.mozilla.org/zhCN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)
在一個對象上定義或修改一個屬性的描述符:
const object1 = {};Object.defineProperty(object1, 'property1', {value: 'ThisIsNotWritable',writable: false});object1.property1 = 'newValue';// throws an error in strict modeconsole.log(object1.property1);// expected output: 'ThisIsNotWritable'
【重點】手寫一個裝飾器函數(shù)
有了上面的兩個基礎(chǔ)后,我們開始利用裝飾器模式的設(shè)計理念,用純函數(shù)的形式寫一個裝飾器,實現(xiàn)記錄函數(shù)耗時功能。為了讓大家更深刻理解裝飾器的原理,我們先不用@Decorator這個語法糖。
下面代碼是本文的重點,大家可以放慢閱讀速度,理解后再繼續(xù)往下看:
// 裝飾器函數(shù)function decoratorLogTime(target, key) {const targetPrototype = target.prototype// Step1 備份原來類構(gòu)造器上的屬性描述符 Descriptorconst oldDescriptor = Object.getOwnPropertyDescriptor(targetPrototype, key)// Step2 編寫裝飾器函數(shù)業(yè)務(wù)邏輯代碼const logTime = function (...arg) {// Before 鉤子let start = +new Date()try {// 執(zhí)行原來函數(shù)return oldDescriptor.value.apply(this, arg) // 調(diào)用之前的函數(shù)} finally {// After 鉤子let end = +new Date()console.log(`耗時: ${end - start}ms`)}}// Step3 將裝飾器覆蓋原來的屬性描述符的 valueObject.defineProperty(targetPrototype, key, {...oldDescriptor,value: logTime})}class GuanYu {attack() {console.log('揮了一次大刀')}run() {console.log('跑了一段距離')}}// Step4 手動執(zhí)行裝飾器函數(shù),裝飾 GuanYu 的 attack 函數(shù)decoratorLogTime(GuanYu, 'attack')// Step4 手動執(zhí)行裝飾器函數(shù),裝飾 GuanYu 的 run 函數(shù)decoratorLogTime(GuanYu, 'run')const guanYu = new GuanYu()guanYu.attack()// 揮了一次大刀// 耗時: 0msguanYu.run()// 跑了一段距離// 耗時: 0ms
以上就是裝飾器的具體實現(xiàn)方法,其核心思路是:
Step1備份原來類構(gòu)造器(Class.prototype) 的屬性描述符(Descriptor)
利用Object.getOwnPropertyDescriptor獲取
Step2 編寫裝飾器函數(shù)業(yè)務(wù)邏輯代碼
利用執(zhí)行原函數(shù)前后鉤子,添加耗時統(tǒng)計邏輯
Step3 用裝飾器函數(shù)覆蓋原來屬性描述符的value
利用Object.defineProperty代理
Step4 手動執(zhí)行裝飾器函數(shù),裝飾Class(類)指定屬性
從而實現(xiàn)在不修改原代碼的前提下,執(zhí)行額外邏輯代碼
作者簡介
阮易強(easonruan)
騰訊高級前端開發(fā)工程師
騰訊高級前端開發(fā)工程師,曾負責過「粵省事」、「穗康」等大型小程序項目,目前是WeDa微搭低代碼平臺(專有版)的核心開發(fā)人員,有豐富低代碼平臺研發(fā)經(jīng)驗。
推薦閱讀
如何用函數(shù)式編程思想優(yōu)化業(yè)務(wù)代碼,這就給你安排上!


