javascript
JS 装饰器(Decorator)场景实战
本文不會大篇幅介紹裝飾器(Decorator)的概念和基礎(chǔ)用法,核心介紹我們團隊如何將裝飾器應(yīng)用于實際開發(fā),和一些高級用法的實現(xiàn)。
裝飾器簡介
Decorator 是 ES7 的一個新語法,正如其“裝飾器”的叫法所表達的,他可以對一些對象進行裝飾包裝然后返回一個被包裝過的對象,可以裝飾的對象包括:類,屬性,方法等。Decorator 的寫法與 Java 里的注解(Annotation)非常類似,但是一定不要把 JS 中的裝飾器叫做是“注解”,因為這兩者的原理和實現(xiàn)的功能還是有所區(qū)別的,在 Java 中,注解主要是對某個對象進行標(biāo)注,然后在運行時或者編譯時,可以通過例如反射這樣的機制拿到被標(biāo)注的對象,對其進行一些邏輯包裝。而 Decorator 的原理和作用則更為簡單,就是包裝對象,然后返回一個新的對象描述(descriptor),其作用也非常單一簡單,基本上就是獲取包裝對象的宿主、鍵值幾個有限的信息。
關(guān)于 Decorator 的詳細介紹參見文章:zhuanlan.zhihu.com/FrontendMag…
簡單來說,JS 的裝飾器可以用來“裝飾”三種類型的對象:類的屬性/方法、訪問器、類本身,簡單看幾個例子吧。
針對屬性/方法的裝飾器
// decorator 外部可以包裝一個函數(shù),函數(shù)可以帶參數(shù) function Decorator(type){/*** 這里是真正的 decorator* @target 裝飾的屬性所述的類的原型,注意,不是實例后的類。如果裝飾的是 Car 的某個屬性,這個 target 的值就是 Car.prototype* @name 裝飾的屬性的 key* @descriptor 裝飾的對象的描述對象*/return function (target, name, descriptor){// 以此可以獲取實例化的時候此屬性的默認值let v = descriptor.initializer && descriptor.initializer.call(this);// 返回一個新的描述對象,或者直接修改 descriptor 也可以return {enumerable: true,configurable: true,get: function() {return v;},set: function(c) {v = c;}}} }復(fù)制代碼注意這里的 target 對應(yīng)的是被裝飾的屬性所屬類的原型,如果是裝飾一個 A 類的屬性,并且 A 類是繼承自 B 類的,這時候你打印 target,獲取到的是 A.prototype,它的結(jié)構(gòu)是這樣的,這里一定要注意:
[image:A944761A-E0FA-4C04-BD90-BE179C46B641-35651-00001223828250C5/187FCC2A-8CC4-46C4-B8A3-A7FD5E0376F6.png]
如果需要操作 target,可能需要搞清楚這個問題。
針對 訪問操作符的裝飾
與屬性方法類似,就不詳述了。
class Person {@nonenumerableget kidCount() { return this.children.length; } }function nonenumerable(target, name, descriptor) {descriptor.enumerable = false;return descriptor; }復(fù)制代碼針對類的裝飾
// 例如 mobx 中 @observer 的用法 /*** 包裝 react 組件* @param target*/ function observer(target) {target.prototype.componentWillMount = function() {targetCWM && targetCWM.call(this);ReactMixin.componentWillMount.call(this);}; }復(fù)制代碼其中的 target 就是類本身(而不是其 prototype)
真實場景應(yīng)用
今天,我們要介紹的主要是,如何將 Decorator 這個特性應(yīng)用于數(shù)據(jù)定義層,實現(xiàn)一些類似于類型檢查、字段映射等功能。
關(guān)于數(shù)據(jù)定義層(Model),其實就是應(yīng)用內(nèi)出現(xiàn)的各種實體數(shù)據(jù)的定義,也就是 MVVM 中的 M 層,注意,和 VM 層做好區(qū)分,Model 本身不提供數(shù)據(jù)的管理和流通,只負責(zé)定義某個實體本身的屬性和方法,例如頁面里有一輛車的模塊,我們就定義一個 CarModel,它用來描述車輛的顏色、價格、品牌等信息。
關(guān)于為什么要在前端應(yīng)用內(nèi)定義明確的 Model,這個我之前在知乎上也早有論述,核心幾點:
- 提高可維護性。將數(shù)據(jù)源頭的實體做一個固定而準(zhǔn)確的描述,這個對于串聯(lián)理解整個應(yīng)用非常重要,特別是在重構(gòu)或者接手別人的代碼的時候,你需要準(zhǔn)確的知道一個頁面(或者是一個模塊)它會包含哪些數(shù)據(jù),這些數(shù)據(jù)分別有哪些字段,這樣更便于理解整個應(yīng)用的數(shù)據(jù)邏輯。
- 提高確定性。當(dāng)你要給你的界面增加幾個車輛字段的時候,你不清楚之前是否已經(jīng)定義過這些字段,服務(wù)端是否會返回這些字段,可能要請求一下(并且要有權(quán)限取到所有字段)才能知道,但是如果有 model 的明確定義,有什么字段就一目了然了。
- 提高開發(fā)效率。在這一層統(tǒng)一做一些數(shù)據(jù)映射和類型檢查等工作,這也是今天要講的重點。
以我們團隊 RN 開發(fā)框架中 Model 部分的實現(xiàn)為例,我們至少提供了三個基礎(chǔ)的基于 Decorator 的功能:類型檢查,單位轉(zhuǎn)換,字段映射。接下來我會先簡單介紹下這幾個功能是做什么的,隨后介紹如何實現(xiàn)這些 Decorator。
先來看看最終調(diào)用時候的代碼
class CarModel extends BaseModel {/*** 價格* @type {number}*/@observable@Check(CheckType.Number)@Unit(UnitType.PRICE_UNIT_WY)price = 0;/*** 賣家名* @type {string}*/@observable@Check(CheckType.String)@ServerName('seller_name')sellerName = ''; }復(fù)制代碼可以看到我們有三個自定義的 decorator :
@Unit, // 單位轉(zhuǎn)換裝飾器 @Check, // 類型檢查裝飾器, @ServerName // 數(shù)據(jù)字段映射裝飾器,當(dāng)前后端定義的字段名不一致的時候用復(fù)制代碼@Unit 是一個比較特殊的裝飾器,它的作用是在前后端之間自動轉(zhuǎn)換單位,也就是前端和后端交換某些帶單位的數(shù)據(jù)的時候,會把根據(jù)各端的注解和裝飾器,把真實值轉(zhuǎn)換成帶單位的值傳給另一端,然后另一端會在框架層自動轉(zhuǎn)成它定義的單位,以此解決前后端單位不一致,交換數(shù)據(jù)時混亂導(dǎo)致的問題。
被 @Unit 裝飾過的屬性,讀寫的時候都是按照前端的單位讀寫,然后再轉(zhuǎn)換成 JSON 的時候就會特殊處理成類似 12.3_$wy 這樣的格式,表示這個數(shù)的單位是萬元。
@Check 更為容易理解,就是用來檢查字段類型,或者檢查字段格式,或者一些自定義檢查,例如正則表達式等。
@ServerName 則用來做映射,例如前后端對同一個界面元素的命名不同,這時候不需要完全按照服務(wù)端的命名來決定,可以在前端用另外一個屬性名,然后將其裝飾成服務(wù)端的字段名。
基礎(chǔ)實現(xiàn)
我們的目標(biāo)就是實現(xiàn)這幾個 Decorator,按照之前對 Decorator 的科普,其實要獨立實現(xiàn)這幾個功能其實非常簡單。
以 @Check 為例,我們改寫被包裝屬性的 descriptor,返回一個新的 descriptor,將被包裝屬性的 getter 和 setter 重新定義,然后在其調(diào)用 setter 的時候先檢查傳入?yún)?shù)的類型和格式,做一些對應(yīng)的處理。
非常簡單,其他幾個 Decorator 的實現(xiàn)也類似,可能像@Unit 這種實現(xiàn)起來會稍顯復(fù)雜,不過只要在 Decorator 中記住每個屬性標(biāo)注的單位,在序列化的時候獲取對應(yīng)的屬性對應(yīng)的單位然后做轉(zhuǎn)換就可以了。
基礎(chǔ)實現(xiàn)的問題
但是,到這里,問題其實還沒有完!
我們的確實現(xiàn)了一個可用的 Decorator,但是這些 Decorator 可以疊加使用嗎?另外可以和業(yè)界常用的一些 Decorator 混用嗎?例如 mobx 中的 @ observable。也就是我上面最開始的實例的用法:
如果你按照我剛才的方式實現(xiàn) @Check 和 @ServerName 的話,你會發(fā)現(xiàn)兩個致命的問題:
- 這兩個自己實現(xiàn)的 Decorator 首先就沒法疊加使用。
- 這兩個 Decorator 都無法和 @observable 這個同時使用。
為什么呢?問題就出在我們改寫屬性的 getter 和 setter 的實現(xiàn)原理上。首先,每次給一個屬性定義 getter 和 setter 都會覆蓋前一次的定義,也就是這個動作只能有一次。然后,mobx 的實現(xiàn)非常依賴對 getter 和 setter 的定義(可以參考我之前的文章:如何自己實現(xiàn)一個 mobx - 原理解析)
事實上,Decorator 本身疊加使用時沒問題的,因為你的每次包裝,都會將屬性的 descriptor 返回給上一層的包裝,最后就是一個函數(shù)包函數(shù)包函數(shù)的效果,最終返回的還是這個屬性的 descriptor 。
進階實現(xiàn)
那我們就需要摒棄掉定義 getter 和 setter 的實現(xiàn)方式。其實除了這種方式,還有很多方式可以實現(xiàn)上述的功能,核心就是一點,在裝飾器函數(shù)里,將你需要處理的屬性和對這個屬性需要做的處理的對應(yīng)關(guān)系都記錄下來,然后在處理實例化數(shù)據(jù)和序列化數(shù)據(jù)的時候,把對應(yīng)關(guān)系取出來,執(zhí)行相關(guān)邏輯即可。
廢話不說,我們直接上一種將這個對應(yīng)關(guān)系掛載到類的原型上的一個實現(xiàn)方式。
function Check (type) {return function (target, name, descriptor) {let v = descriptor.initializer && descriptor.initializer.call(this);/*** 將屬性名字以及需要的類型的對應(yīng)關(guān)系記錄到類的原型上*/if (!target.constructor.__checkers__) {// 將這個隱藏屬性定義成 not enumerable,遍歷的時候是取不到的。Object.defineProperty(target.constructor, "__checkers__", {value: {},enumerable: false,writeable: true,configurable: true});}target.constructor.__checkers__[name] = {type: type};return descriptor} }復(fù)制代碼注意,我前面提到的一個信息,裝飾函數(shù)的第一個參數(shù) target 是包裝屬性所屬的類的原型(prototype),這個通過看 babel 編譯后的結(jié)果可以看到。然后我這里為什么將對應(yīng)關(guān)系掛載到 target.constructor 上,是因為我所有的 Model 類,都是繼承自我提供的一個 Model 基類的(BaseModel),target 拿到的不是子類的原型,而是基類的原型,target.constructor 拿到的才是最終的子類。也就是我把對應(yīng)關(guān)系掛載到了開發(fā)定義的子類上。
接下來看看基類的代碼,核心提供兩個方法,分別是映射數(shù)據(jù)和序列化的方法。
class BaseModel {/*** 將后端數(shù)據(jù)直接映射到當(dāng)前的示例上*/__map (json) {let alias = this.constructor.__aliasNames__;let units = this.constructor.__unitOriginals__;let checkers = this.constructor.__checkers__;for (let i in this) {if (!this.hasOwnProperty(i)) return;// 如果有多層裝飾器,需要經(jīng)過多個邏輯處理最終產(chǎn)生一個最終值 realValuelet realValue = json[i];// 接下來一步一步處理數(shù)據(jù)// 首先檢查別名數(shù)據(jù),并做映射if (alias && typeof(alias[i]) !== 'undefined') {// ......}// 然后針對數(shù)據(jù)檢查類型if (checkers && checkers[i]) {// ......}// 最終,對數(shù)據(jù)做單位轉(zhuǎn)換if (units && units[i]) {// ......}// 賦值this[i] = realValue;}}/*** 復(fù)寫 JSON.stringify 時自動調(diào)用的函數(shù)*/toJSON () {let result = {};let units = this.constructor.__unitOriginals__;for (let i in this) {if (!this.hasOwnProperty(i)) return;if (units && units[i]) {// 序列化時,有需要加單位的加上單位result[i] = this[i] + '_$' + units[i];} else {result[i] = this[i];}}return result;} }復(fù)制代碼在 __map 函數(shù)中,我們將當(dāng)前類(this.constructor)上的對應(yīng)關(guān)系都取出來,然后做數(shù)據(jù)校驗和映射,這里應(yīng)該不難理解了。
最終應(yīng)用的代碼就是我們開篇貼出來最終使用的代碼,只要相應(yīng)的 Model 類繼承自 BaseModel 即可。
通過這樣的方式實現(xiàn)的 Decorator ,因為沒有用到任何 getter setter 相關(guān)的功能,所以可以和 mobx 這樣的庫完美融合,并且可以無限疊加使用,不過如果你用到了多個三方庫,他們都提供了對應(yīng)的 Decorator,然后又都修改了 getter 和 setter,那就沒有辦法了!
總結(jié)
Decorator 雖然原理非常簡單,但是的確可以實現(xiàn)很多實用又方便的功能,目測前端領(lǐng)域很多框架和庫都會大規(guī)模使用這個特性,但是也希望這些庫在實現(xiàn) Decorator 的時候考慮下通用性,考慮下疊加和共存的問題。像上面 mobx 的 @observable,不關(guān)無法疊加,而且和我自己實現(xiàn)的 Decorator 的順序都不能亂,必須在最外層,因為它改變了整個屬性的性質(zhì),不寫在最外層的時候,會發(fā)現(xiàn)一些莫名其妙的問題。
總結(jié)
以上是生活随笔為你收集整理的JS 装饰器(Decorator)场景实战的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从Adobe Photoshop CC
- 下一篇: 【Android开发坑系列】之事件