JS 裝飾器(Decorator)場景實戰

小芋頭君發表於2017-10-26

本文不會大篇幅介紹裝飾器(Decorator)的概念和基礎用法,核心介紹我們團隊如何將裝飾器應用於實際開發,和一些高階用法的實現。


裝飾器簡介

Decorator 是 ES7 的一個新語法,正如其“裝飾器”的叫法所表達的,他可以對一些物件進行裝飾包裝然後返回一個被包裝過的物件,可以裝飾的物件包括:類,屬性,方法等。Decorator 的寫法與 Java 裡的註解(Annotation)非常類似,但是一定不要把 JS 中的裝飾器叫做是“註解”,因為這兩者的原理和實現的功能還是有所區別的,在 Java 中,註解主要是對某個物件進行標註,然後在執行時或者編譯時,可以通過例如反射這樣的機制拿到被標註的物件,對其進行一些邏輯包裝。而 Decorator 的原理和作用則更為簡單,就是包裝物件,然後返回一個新的物件描述(descriptor),其作用也非常單一簡單,基本上就是獲取包裝物件的宿主、鍵值幾個有限的資訊。

關於 Decorator 的詳細介紹參見文章:zhuanlan.zhihu.com/FrontendMag…

簡單來說,JS 的裝飾器可以用來“裝飾”三種型別的物件:類的屬性/方法、訪問器、類本身,簡單看幾個例子吧。

針對屬性/方法的裝飾器

// decorator 外部可以包裝一個函式,函式可以帶引數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;

}
}
}
}複製程式碼

注意這裡的 target 對應的是被裝飾的屬性所屬類的原型,如果是裝飾一個 A 類的屬性,並且 A 類是繼承自 B 類的,這時候你列印 target,獲取到的是 A.prototype,它的結構是這樣的,這裡一定要注意:

[image:A944761A-E0FA-4C04-BD90-BE179C46B641-35651-00001223828250C5/187FCC2A-8CC4-46C4-B8A3-A7FD5E0376F6.png]
如果需要操作 target,可能需要搞清楚這個問題。

針對 訪問操作符的裝飾

與屬性方法類似,就不詳述了。

class Person { 
@nonenumerable get kidCount() {
return this.children.length;

}
}function nonenumerable(target, name, descriptor) {
descriptor.enumerable = false;
return descriptor;

}複製程式碼

針對類的裝飾

// 例如 mobx 中 @observer 的用法/** * 包裝 react 元件 * @param target */function observer(target) { 
target.prototype.componentWillMount = function() {
targetCWM &
&
targetCWM.call(this);
ReactMixin.componentWillMount.call(this);

};

}複製程式碼

其中的 target 就是類本身(而不是其 prototype)


真實場景應用

今天,我們要介紹的主要是,如何將 Decorator 這個特性應用於資料定義層,實現一些類似於型別檢查、欄位對映等功能。

關於資料定義層(Model),其實就是應用內出現的各種實體資料的定義,也就是 MVVM 中的 M 層,注意,和 VM 層做好區分,Model 本身不提供資料的管理和流通,只負責定義某個實體本身的屬性和方法,例如頁面裡有一輛車的模組,我們就定義一個 CarModel,它用來描述車輛的顏色、價格、品牌等資訊。

關於為什麼要在前端應用內定義明確的 Model,這個我之前在知乎上也早有論述,核心幾點:

  • 提高可維護性。將資料來源頭的實體做一個固定而準確的描述,這個對於串聯理解整個應用非常重要,特別是在重構或者接手別人的程式碼的時候,你需要準確的知道一個頁面(或者是一個模組)它會包含哪些資料,這些資料分別有哪些欄位,這樣更便於理解整個應用的資料邏輯。
  • 提高確定性。當你要給你的介面增加幾個車輛欄位的時候,你不清楚之前是否已經定義過這些欄位,服務端是否會返回這些欄位,可能要請求一下(並且要有許可權取到所有欄位)才能知道,但是如果有 model 的明確定義,有什麼欄位就一目瞭然了。
  • 提高開發效率。在這一層統一做一些資料對映和型別檢查等工作,這也是今天要講的重點。

以我們團隊 RN 開發框架中 Model 部分的實現為例,我們至少提供了三個基礎的基於 Decorator 的功能:型別檢查,單位轉換,欄位對映。接下來我會先簡單介紹下這幾個功能是做什麼的,隨後介紹如何實現這些 Decorator。

先來看看最終呼叫時候的程式碼

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 = '';

}複製程式碼

可以看到我們有三個自定義的 decorator :

@Unit,         // 單位轉換裝飾器@Check,        // 型別檢查裝飾器,@ServerName    // 資料欄位對映裝飾器,當前後端定義的欄位名不一致的時候用複製程式碼

@Unit 是一個比較特殊的裝飾器,它的作用是在前後端之間自動轉換單位,也就是前端和後端交換某些帶單位的資料的時候,會把根據各端的註解和裝飾器,把真實值轉換成帶單位的值傳給另一端,然後另一端會在框架層自動轉成它定義的單位,以此解決前後端單位不一致,交換資料時混亂導致的問題。

被 @Unit 裝飾過的屬性,讀寫的時候都是按照前端的單位讀寫,然後再轉換成 JSON 的時候就會特殊處理成類似 12.3_$wy 這樣的格式,表示這個數的單位是萬元。
@Check 更為容易理解,就是用來檢查欄位型別,或者檢查欄位格式,或者一些自定義檢查,例如正規表示式等。
@ServerName 則用來做對映,例如前後端對同一個介面元素的命名不同,這時候不需要完全按照服務端的命名來決定,可以在前端用另外一個屬性名,然後將其裝飾成服務端的欄位名。

基礎實現

我們的目標就是實現這幾個 Decorator,按照之前對 Decorator 的科普,其實要獨立實現這幾個功能其實非常簡單。
以 @Check 為例,我們改寫被包裝屬性的 descriptor,返回一個新的 descriptor,將被包裝屬性的 getter 和 setter 重新定義,然後在其呼叫 setter 的時候先檢查傳入引數的型別和格式,做一些對應的處理。

/** * 此註解如果賦值的時候匹配到的型別有問題,會在控制檯顯示警告 * @param type CheckType 中定義的型別 * @returns {Function
} * @constructor */
function CheckerDecorator(type){
return function (target, name, descriptor){
let v = descriptor.initializer &
&
descriptor.initializer.call(this);
return {
enumerable: true, configurable: true, get: function() {
return v;

}, set: function(c) {
// 在此對傳入的 c 的值做各種檢查 var cType = typeof(c);
// ... v = c;

}
}
}
}複製程式碼

非常簡單,其他幾個 Decorator 的實現也類似,可能像@Unit 這種實現起來會稍顯複雜,不過只要在 Decorator 中記住每個屬性標註的單位,在序列化的時候獲取對應的屬性對應的單位然後做轉換就可以了。

基礎實現的問題

但是,到這裡,問題其實還沒有完!
我們的確實現了一個可用的 Decorator,但是這些 Decorator 可以疊加使用嗎?另外可以和業界常用的一些 Decorator 混用嗎?例如 mobx 中的 @ observable。也就是我上面最開始的例項的用法:

@observable@Check(CheckType.String)@ServerName('seller_name')sellerName = '';
複製程式碼

如果你按照我剛才的方式實現 @Check 和 @ServerName 的話,你會發現兩個致命的問題:

  • 這兩個自己實現的 Decorator 首先就沒法疊加使用。
  • 這兩個 Decorator 都無法和 @observable 這個同時使用。
    為什麼呢?問題就出在我們改寫屬性的 getter 和 setter 的實現原理上。首先,每次給一個屬性定義 getter 和 setter 都會覆蓋前一次的定義,也就是這個動作只能有一次。然後,mobx 的實現非常依賴對 getter 和 setter 的定義(可以參考我之前的文章:如何自己實現一個 mobx – 原理解析

事實上,Decorator 本身疊加使用時沒問題的,因為你的每次包裝,都會將屬性的 descriptor 返回給上一層的包裝,最後就是一個函式包函式包函式的效果,最終返回的還是這個屬性的 descriptor 。

進階實現

那我們就需要摒棄掉定義 getter 和 setter 的實現方式。其實除了這種方式,還有很多方式可以實現上述的功能,核心就是一點,在裝飾器函式裡,將你需要處理的屬性和對這個屬性需要做的處理的對應關係都記錄下來,然後在處理例項化資料和序列化資料的時候,把對應關係取出來,執行相關邏輯即可。

廢話不說,我們直接上一種將這個對應關係掛載到類的原型上的一個實現方式。

function Check (type) { 
return function (target, name, descriptor) {
let v = descriptor.initializer &
&
descriptor.initializer.call(this);
/** * 將屬性名字以及需要的型別的對應關係記錄到類的原型上 */ 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
}
}複製程式碼

注意,我前面提到的一個資訊,裝飾函式的第一個引數 target 是包裝屬性所屬的類的原型(prototype),這個通過看 babel 編譯後的結果可以看到。然後我這裡為什麼將對應關係掛載到 target.constructor 上,是因為我所有的 Model 類,都是繼承自我提供的一個 Model 基類的(BaseModel),target 拿到的不是子類的原型,而是基類的原型,target.constructor 拿到的才是最終的子類。也就是我把對應關係掛載到了開發定義的子類上。

接下來看看基類的程式碼,核心提供兩個方法,分別是對映資料和序列化的方法。

class BaseModel { 
/** * 將後端資料直接對映到當前的示例上 */ __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;
// 如果有多層裝飾器,需要經過多個邏輯處理最終產生一個最終值 realValue let realValue = json[i];
// 接下來一步一步處理資料 // 首先檢查別名資料,並做對映 if (alias &
&
typeof(alias[i]) !== 'undefined') {
// ......
} // 然後針對資料檢查型別 if (checkers &
&
checkers[i]) {
// ......
} // 最終,對資料做單位轉換 if (units &
&
units[i]) {
// ......
} // 賦值 this[i] = realValue;

}
} /** * 複寫 JSON.stringify 時自動呼叫的函式 */ 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;

}
}複製程式碼

在 __map 函式中,我們將當前類(this.constructor)上的對應關係都取出來,然後做資料校驗和對映,這裡應該不難理解了。

最終應用的程式碼就是我們開篇貼出來最終使用的程式碼,只要相應的 Model 類繼承自 BaseModel 即可。

通過這樣的方式實現的 Decorator ,因為沒有用到任何 getter setter 相關的功能,所以可以和 mobx 這樣的庫完美融合,並且可以無限疊加使用,不過如果你用到了多個三方庫,他們都提供了對應的 Decorator,然後又都修改了 getter 和 setter,那就沒有辦法了!


總結

Decorator 雖然原理非常簡單,但是的確可以實現很多實用又方便的功能,目測前端領域很多框架和庫都會大規模使用這個特性,但是也希望這些庫在實現 Decorator 的時候考慮下通用性,考慮下疊加和共存的問題。像上面 mobx 的 @observable,不關無法疊加,而且和我自己實現的 Decorator 的順序都不能亂,必須在最外層,因為它改變了整個屬性的性質,不寫在最外層的時候,會發現一些莫名其妙的問題。

來源:https://juejin.im/post/59f1c484f265da431c6f8940#heading-2

相關文章