JS 裝飾器(Decorator)場景實戰

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


裝飾器簡介

Decorator 是 ES7 的一個新語法,正如其「裝飾器」的叫法所表達的,他能夠對一些對象進行裝飾包裝而後返回一個被包裝過的對象,能夠裝飾的對象包括:類,屬性,方法等。Decorator 的寫法與 Java 裏的註解(Annotation)很是相似,可是必定不要把 JS 中的裝飾器叫作是「註解」,由於這二者的原理和實現的功能仍是有所區別的,在 Java 中,註解主要是對某個對象進行標註,而後在運行時或者編譯時,能夠經過例如反射這樣的機制拿到被標註的對象,對其進行一些邏輯包裝。而 Decorator 的原理和做用則更爲簡單,就是包裝對象,而後返回一個新的對象描述(descriptor),其做用也很是單一簡單,基本上就是獲取包裝對象的宿主、鍵值幾個有限的信息。前端

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

簡單來講,JS 的裝飾器能夠用來「裝飾」三種類型的對象:類的屬性/方法、訪問器、類自己,簡單看幾個例子吧。react

針對屬性/方法的裝飾器

// 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,可能須要搞清楚這個問題。json

針對 訪問操做符的裝飾

與屬性方法相似,就不詳述了。後端

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)bash


真實場景應用

今天,咱們要介紹的主要是,如何將 Decorator 這個特性應用於數據定義層,實現一些相似於類型檢查、字段映射等功能。babel

關於數據定義層(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 的順序都不能亂,必須在最外層,由於它改變了整個屬性的性質,不寫在最外層的時候,會發現一些莫名其妙的問題。

相關文章
相關標籤/搜索