【用故事解讀 MobX 源碼(四)】裝飾器 和 Enhancer

================前言===================html

=======================================git

按照步驟,這篇文章應該寫 觀察值(Observable)的,不過在撰寫的過程當中發現,若是不先搞明白裝飾器和 Enhancer(對這個單詞陌生的,先不要着急,繼續往下看) ,直接去解釋觀察值(Observable)會很費勁。由於在 MobX 中是使用裝飾器設計模式實現觀察值的,因此說要先掌握裝飾器,才能進一步去理解觀察值。es6

因此這是一篇 「插隊」 的文章,用於去理解 MobX 中的裝飾器和 Enhancer 概念。github

A. 本文目標

本文主要解決我我的在源碼閱讀中的疑惑:npm

  • 在官方文檔 如何(不)使用裝飾器 中,爲何說開啓 @observable@computer 等裝飾器語法,是和直接使用 decorate 是等效的?
  • 在 MobX 源碼中時常出現的 Enhancer 究竟是個什麼概念?它在 MobX 體系中發揮怎樣的做用?它和裝飾器又是怎麼樣的一層關係?

若是你也有這樣的疑惑,不妨繼續閱讀本文,歡迎一塊兒討論。編程

至於 觀察值(Observable),在本文中你只要掌握住 官方文檔 observable 的用法就足夠了,好比(示例摘自官方文檔):json

const person = observable({
    firstName: "Clive Staples",
    lastName: "Lewis"
});
person.firstName = "C.S.";

const temperature = observable.box(20);
temperature.set(25);

對於 observable 方法的源碼解析將在下一篇中詳細展開,此篇文章不會作過多的討論。segmentfault

B. 學會裝飾器

一、裝飾器基礎知識

和其餘語言(Python、Java)同樣,裝飾器語法是藉助 @ 符號實現的,如今問題就歸結到如何用 JS 去實現 @ 語法。設計模式

對於還不熟悉裝飾器語法的讀者,這裏推薦文章 《ES7 Decorator 裝飾者模式》,以鋼鐵俠爲例,經過裝備特殊的裝備就能將普通人變成鋼鐵俠,簡單歸納起來就是:

clipboard.png

裝飾器設計模式的理念就和上面那樣的樸素,在不改造 託尼·史塔克(Tony Stark) 本體的前提下,經過加裝 盔甲飛行器 的方式加強 Tony 的能力,從而「變成」鋼鐵俠。

有關裝飾器使用的文章,還能夠參考這兩篇參考文章 探尋 ECMAScript 中的裝飾器 Decorator細說ES7 JavaScript Decorators

文章都比較早,當時寫文章的做者都認爲在新的 ES7 裏會推出標準的 @ 語法,然而過後證實官方並無這個意願。咱們知道目前的 ECMAScript 2015 標準,甚至到 ECMAScript 2018 標準官方都沒有提供 @ 語法的支持,咱們在其餘文章中看到的 @ 語法都是經過 babel 插件來實現的。

上面說起的參考文章都是屬於應用類型的,就是直接使用裝飾器語法(即直接使用 @ 語法)來展現裝飾器的實際應用,而對於如何實現 @ 語法並無說起 —— 那就是如何用 Object.defineProperty 來實現 @ 語法。

道理你們都懂,那麼到底如何才能本身動手去實現 @ 裝飾器語法呢?

二、首先你要理解屬性描述符(descriptor)

在 JS 中,咱們藉助 Object.defineProperty 方法實現裝飾器設計模式,該方法簽名以下:

Object.defineProperty(obj, prop, descriptor)

其中最核心的實際上是 descriptor —— 屬性描述符

屬性描述符總共分兩種:數據描述符(Data descriptor)和 訪問器描述符(Accessor descriptor)。

描述符必須是兩種形式之一,但不能同時是二者

好比 數據描述符

Object.getOwnPropertyDescriptor(user,'name');

// 輸出
/**
{
  "value": "張三",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
**/

還有 訪問器描述符

var anim = { 
  get age() { return 5; } 
};
Object.getOwnPropertyDescriptor(anim, "age");
// 輸出
/**
{
   configurable: true,
   enumerable: true,
   get: /*the getter function*/,
   set: undefined
 }
**/

具體可參考 StackOverflow 上的問答 What is a descriptor?

接下來,咱們一塊兒來看一下 babel 中究竟是如何實現 @ 語法的?

三、搭建裝飾器的 babel 示例

在理解屬性描述符的基礎上,咱們就能夠去看看 babel 對於裝飾器 @ 語法的內部實現了。

就拿 MobX 官方的示例 來說:

import { observable, computed, action } from "mobx";

class OrderLine {
    @observable price = 0;
    @observable amount = 1;

    @computed get total() {
        return this.price * this.amount;
    }
    
    @action.bound
    increment() {
        this.amount++ // 'this' 永遠都是正確的
    }
}

咱們並非真正想要運行上面那段代碼,而是想看一下 babel 經過裝飾器插件,把上面那段代碼中的 @ 語法轉換成什麼樣子了。

運行這段代碼須要搭建 babel 環境,因此直接扔到瀏覽器運行會報錯的。按照官方文檔 如何(不)使用裝飾器 中的提示,須要藉助 babel-preset-mobx 插件,這是一個預設(preset,至關於 babel 插件集合),真正和裝飾器有關的是插件是 babel-plugin-transform-decorators-legacy

四、有兩種方式看轉換以後的代碼

4.一、 方法一,使用 babel 在線工具

放到 babel 在線工具,粘貼現有的示例代碼會報錯,不過 babel 給出了友好的提示,由於使用到了裝飾器語法,須要安裝 babel-plugin-transform-decorators-legacy

decorator

咱們點擊左下方的 Add Plugin 按鈕,在彈出的搜索框裏輸入關鍵字 decorators-legacy,選擇這個插件就能夠:

keywords

選完插件以後,代碼就會成功轉譯:

transform

底下會提示 require is not defined 錯誤,這個錯誤並不影響你分析裝飾器的語法,由於有 @ 符號部分都已經轉換成 ES5 語法了,只是這個報錯沒法讓這段示例代碼運行起來。

這是由於 Babel 只是將最新的 ES6 語法「翻譯」成各大瀏覽器支持比較好的 ES5 語法,但模塊化寫法( require語句)自己就不是 ECMAScript 的標準,而是產生了其餘的模塊化寫法標準,例如 CommonJS,AMD,UMD。所以 Babel 轉碼模塊化寫法後在瀏覽器中仍是沒法運行,此時能夠考慮放到 Webpack 這種自動化構建工具環境中,此時 Webpack 是支持模塊化寫法的

若是有強迫症的同窗,非得想要這段代碼運行起來,能夠參考下述的 方法二

4.二、方法二,使用 demo 工程

官方提供了 mobx-react-boilerplate,clone 下來以後直接:

npm install
npm start
說明:package.json 中的 dependencies 字段比較陳舊了,能夠本身手動更新到最新版本

打開控制檯就能夠看到 bundle.js 文件了:

bundle

這樣,咱們就能夠直接在 index.js 中粘貼咱們須要的代碼,由於基於 Webpack 打包,因此示例代碼是能夠運行的。

五、分析轉換以後的代碼邏輯

上述兩種方法由於都是使用同一個裝飾器轉換插件 babel-plugin-transform-decorators-legacy,因此裝飾器語法部分轉換後的代碼是同樣的

好比針對 price 屬性的裝飾器語法:

@observable price = 0;

通過 babel 轉譯以後:

var _descriptor = _applyDecoratedDescriptor(
    _class.prototype,
    'price',
    [_mobx.observable],
    {
      enumerable: true,
      initializer: function initializer() {
        return 0;
      }
    }
  )

而對於 total 方法的裝飾器語法:

@computed get total() {
    return this.price * this.amount;
}

通過 babel 轉譯以後則爲:

_applyDecoratedDescriptor(
  _class.prototype,
  'total',
  [_mobx.computed],
  Object.getOwnPropertyDescriptor(_class.prototype, 'total'),
  _class.prototype
);

apply

能夠看到關鍵是使用了 _applyDecoratedDescriptor 方法。接下來咱們着重分析這個方法。

六、關鍵是 _applyDecoratedDescriptor 方法

該函數簽名爲:

function _applyDecoratedDescriptor(
  target,
  property,
  decorators,
  descriptor,
  context
)

具體的用法,以 price 屬性爲例,咱們能夠獲取對應的實參:

  • target_class.prototype ,即 OrderLine.prototype
  • property:即字符串 "price"
  • decorators:在這裏是 [_mobx.observable](不一樣的修飾符裝飾器是不同的,好比使用 @computed 修飾的 total 方法,就是 [_mobx.computed]),是長度爲 1 的數組,具體的 observable 方法將在下一篇文章詳細講,就是 createObservable
  • descriptor:即屬性描述符,屬性成員(好比 price)會有 initializer 屬性,而方法成員(好比 total) 則不會有這個屬性,用這個來區分這兩種不一樣屬性描述符。
{
  enumerable: true,
  initializer: function initializer() {
    return 0;
  }
}
  • context:就是運行上下文,通常來說對數據屬性的裝飾則爲 null,對方法屬性則是 _class.prototype

看完函數簽名,咱們繼續看函數內容:

_applyDecoratedDescriptor

這幾行代碼沒啥難度,就是咱們熟悉的 屬性描述符 相關的內容:

  • 圖中標註 ① ,表示返回的 desc 變量就是咱們熟悉的 屬性描述符。所以,該 _applyDecoratedDescriptor 的做用就是根據入參返回具體的描述符。
  • 若是是屬性成員(好比price),就將返回的描述符就能夠傳給 _initDefineProp (至關於 Object.defineProperty)應用到原來的屬性中去了,從而起到了 裝飾 做用。

applyDecorated

  • 圖中標註 ② ,表示對於方法成員(好比 total)則直接應用 Object.defineProperty 方法(當是方法成員時,desc 是沒有 initializer 屬性的),同時令 desc = null,從後續的應用來看並不會和 _initDefineProp 方法搭配使用

對於圖中標註 ③ ,咱們具體看decorators 在其中發揮的做用,典型的函數式編程手法:

decorators

  • 首先總體上來看,是一個循環語句。假如咱們傳入的 decorators[a, b, c],那麼上面的代碼至關於應用公式 a(b(c(property))),也就是裝飾器 c 先裝飾屬性 property,隨後再疊加裝飾器 b 的做用,最後疊加裝飾器 a。以 price 屬性爲例,因爲只有一個裝飾器(@observable),因此只應用了 [_mobx.observable] 這一個裝飾器。
  • 其次局部來看,裝飾器具體應用表達式是 decorator(target, property, desc) ,其函數簽名和 Object.defineProperty 是如出一轍。經過圖中標註 ③ 咱們能夠理解,當咱們寫裝飾器函數函數時,函數的定義入參必須是 (target, name, descriptor) 這樣的,同時該函數必需要返回屬性描述符。(能夠停下來去翻翻看本身寫裝飾器函數的那些例子)

至此咱們已經掌握了 babel 轉換 @ 語法的精髓 —— 建立了 _applyDecoratedDescriptor 方法,從而依次應用你所定義的裝飾器方法,並且也明白了自定義的裝飾器方法的函數簽名必須是 (target, name, descriptor) 的。

總結一下這個 babel 插件對於裝飾器語法 @ 所作的事情:

  1. 經過 ast 分析,將 @ 語法轉換成 _applyDecoratedDescriptor 方法的應用
  2. _applyDecoratedDescriptor 方法就是一個循環應用裝飾器的過程

那麼接下來咱們回到主題,mobx 若是不使用 babel 轉譯,那該如何實現相似於上述裝飾器的語法呢?

七、不用裝飾器語法,mobx 提供了等價寫法

很顯然,MobX 不能實現(也沒有必要)ast 分析將 @ 語法轉換掉的功能,因此只能提供 循環應用裝飾器 的這方面的功能。

爲達到這個目的,MobX 4.x 版本相對 3.x 等之前版本多了 decorate API 方法。

官方文檔 如何(不)使用裝飾器 所言,使用裝飾器 @ 語法等價於使用 decorate 方法,即改寫成以下形式:

import { observable, computed, decorate, action } from "mobx";

class OrderLine {
    price = 0;
    amount = 1;

    get total() {
        return this.price * this.amount;
    }
}
decorate(OrderLine, {
    price: observable,
    amount: observable,
    total: computed,
    increment: action.bound
})
3.x 之前的版本由於沒有 decorate 方法,因此是藉助 extendObservable 方法實現的,具體見文檔 在ES五、ES6和ES.next環境下使用 MobX

咱們翻開 decorate 源碼,該函數聲明是:

decorate(thing, decorators)
  • thing:須要被裝飾的原始對象;
  • decorators:裝飾器配置對象,是一個 key/value 形式的對象, key 是屬性名,value 就是具體的裝飾器函數(好比 observablecomputedaction.bound 這樣具體的裝飾器有效函數)

摘出核心語句:

decorate

能夠看去的確就是一個 for 循環,而後依次應用 decorator,這剛好就是 babel 插件轉換後 _applyDecoratedDescriptor 方法所作的事情,所以二者是等效的。

這樣,就解答了本文開篇提出的第一個疑問。 @observable@computer 等裝飾器語法,是和直接使用 decorate 是等效等價的。

看到這裏是否是以爲有點兒難以想象?嗯,事實上裝飾器應用的過程就這麼的簡單。你也能夠直接將這個 decorate API 方法直接提取到本身的項目中使用,給你的項目增長新的 feature。

解答完第一個問題,咱們繼續講本文開頭提出的另外一個問題:MobX 中的 enhancer 是什麼概念?

C. 理解 Enhancer

一、Enhancer 概念

Enhancer 這個概念是 MobX 本身提出的一個概念,剛接觸到的用戶大多數會先蒙圈一下子。

學習過 MobX 3.x 及之前版本的人可能會遇到 Modifier 這個概念,Enhancer 其實就是 Modifier

Modifier 在 MobX 3 以前的版本里官方有專門的 文檔 解說。不過到 MobX 4.x 以後官方就刪除了這篇文檔。好在這個概念是內部使用的,修更名字對外部調用者沒有啥影響。

Enhancer 從字面上理解是 加強器,其做用就是給原有的對象 增長額外的功能 —— 這不就是裝飾器的做用麼?沒錯,它是輔助 MobX 中的 @observable 裝飾器功能的。結合裝飾器,會更加容易理解這個概念。

二、Enhancer 和 @observable 的總體關係

MobX 不是有不少種裝飾器麼,好比 @observable@compute@action,注意 Enhancer 只和 @observable 有關係,和 @compute@action 是沒啥關係的。這是由於 Enhancer 是爲觀察值(observable)服務的,和計算值(computedValue)和動做(Action)不要緊。

@observable 裝飾器中真正起做用的函數就是 Enhancer ,你能夠將 Enhancer 理解成 @observable 裝飾器有效的那部分。能夠用 "藥物膠囊💊" 來理解 @observable 裝飾器和 Enhancer 的關係:

形象的類比

  • @observable 裝飾器就像是膠囊的外殼,內裏攜帶的藥物成分就是 Enhancer,由於真正起效果的部分是 Enhancer
  • 平時咱們所接觸到的 @observable 裝飾器僅僅是起到包裝、傳輸到指定目的地的做用。
  • 從另外一個角度來說,在 mobx 代碼實現中,Enhancer 是實現 Observable 觀察值必不可少的一部分,沒有它就實現不了觀察值功能,也就構建不起 MobX 體系了;而若是缺失 @observable 相關的代碼,頂可能是不能使用裝飾器功能而已。
  • 這裏還要特別強調一下,這裏特指 @observable 裝飾器是這種狀況,其餘的裝飾器(包括 @compute@action 這樣的裝飾器以及本身寫的裝飾器)都不在此討論範疇

在 MobX 中有 4 種 Enhancer,在 types/modifier.ts 中有定義:

  • deepEnhancer:默認的,也是最經常使用的,它會遞歸地在可觀察對象的屬性或可觀察數組、Map 的元素上調用;
  • shallowEnhancer:不對傳入的值進行轉換,直接返回
  • referenceEnhancer:只轉換 Object, Array, Map 自己,不對其屬性(或元素)轉換
  • refStructEnhancer:結構內容值發生改變的時候才進行數據更新

不理解的話能夠參考 Mobx 源碼解讀(三) Modifier 文章,有詳細的示例解說,本文就不展開了。

接下來,咱們須要解決的是有兩個問題:

  1. Enhancer 是如何和 @observable 裝飾器語法產生聯繫的?
  2. Enhancer 真正起做用是在什麼地方?

三、Enhancer 是如何運用到 @observable 裝飾器語法中的?

這個過程講解起來有點兒繞。但我仍是儘量講得明白一些吧。

返回看上面示例中:

@observable price = 0;

該裝飾語法最終會換成 _mobx.observable 方法的調用。

咱們看一下 observable 源碼

export const observable: IObservableFactory &
    IObservableFactories & {
        enhancer: IEnhancer<any>
    } = createObservable as any

會發現 observable 是函數,其函數內容就是 createObservable

所以上面示例中轉義後的代碼至關於:

return createObservable(OrderLine.prototype, 'price', desc);

繼續看這個 createObservable 大致邏輯走向,該方法依據 第二個參數是否 string 類型 而起到不一樣的做用:

two way

  • 若是第二個參數不是 string 類型,會走圖中所示 ① 的邏輯,至關於 轉換函數,將普通屬性轉換成 Observable 對象;這部分邏輯咱們下一篇文章會着重講到,這裏暫且略過;
  • 若是第二個參數是 string 類型 ,那麼就是本文所述起到 裝飾器 做用,此時方法第二個入參必須是 string,從而會調用 deepDecorator.apply(null, arguments),這是咱們這篇文章要繼續講的內容。

探究一下 deepDecorator 的來歷:

const deepDecorator = createDecoratorForEnhancer(deepEnhancer)

經過給 createDecoratorForEnhancer 方法傳入 deepEnhancer 就能夠了。從這個 createDecoratorForEnhancer 方法的名字就能知道其含義,基於 enhancer 建立裝飾器,是否是有點神奇,直接用 Enhancer 就能建立到對應的裝飾器了!MobX 中其餘 enhancer 也是基於這個函數建立相應的裝飾器的:

create

這個過程就是 @observable 裝飾器語法 和 enhancer 產生聯繫的地方。

四、Enhancer 真正起做用是在什麼地方?

繼續研究 createDecoratorForEnhancer 方法就能探知 Enhancer 起做用的地方。

不過接下來的函數分解,涉及到各類閉包來回整,很容易把人繞暈。這裏作了一副簡單的調用順序圖:

create

  • createDecoratorForEnhancer 裏面會調用 createPropDecorator
  • createPropDecorator 方法執行的時候會調用 defineObservableProperty 方法,createPropDecorator 是一個閉包,因此 defineObservableProperty 能在做用域中獲知 enhancer 變量
  • defineObservableProperty 中會繼續調用 new ObservableValue 建立觀察值,建立的過程當中會將 enhancer 做爲參數傳遞進去。

這裏就不展開講解,看得很暈也不用在乎,有個大概瞭解就行。感興趣的讀者,能夠挨個在源碼中查找上述的函數名字,感覺他們互相調用的關係,外加再看一下 defineObservableProperty 源碼就能夠。

下一篇文章着重分析觀察值(Observable)過程的時候,還會涉及這部分邏輯,這裏咱們知道大體的結論就行:最終的 enhancer 會傳遞給 ObservableValue 構造函數,從而影響觀察值建立過程

具體的影響在 ObservableValue 的構造函數中就體現出來,直接影響觀察值對象中的 value 屬性:

this.value = enhancer(value, undefined, name)

observable

再結合 types/modifier.ts 中有各類 Enhancer 的具體內容,就能大體瞭解 enhancer 是如何起到 轉換數值 的做用的,再分析下去就是觀察值(Observable)的內容了,由於裏面涉及到 遞歸轉換 的邏輯,因此我統一會放在下一篇文章中展開講解。

本文小結

在不用 babel 轉義的狀況下,mobx 經過提供decorate API 實現等價裝飾器功能,原理也很簡單:

  • 裝飾器方法的函數簽名必須是 (target, property, desc)(某種意義上已經成規範了)
  • 先從對象中獲取屬性成員(或方法成員)的原始 屬性描述符
  • 將屬性描述符傳給裝飾器方法,獲取更改後的 屬性描述符
  • 經過 Object.defineProperty 將更改後的屬性描述符 「安裝」 回原始對象
  • 如有多個裝飾器,就循環上述過程。

歸納起來就是 循環應用裝飾器方法,就是那麼簡單粗暴有效。

能夠看一下官方針對裝飾器的 免責聲明

至於 Enhancer,它隻影響觀察值(Observable)的生成,不一樣的 Enhancer 會造成不一樣種類的觀察值(Observable);

正是由於 Enhancer 隻影響觀察值(Observable),因此和它相關的裝飾器只有 @observable,與 @computed 以及 @action 等裝飾器無關(不過裝飾器方法的定義都大同小異,只是有效成分不同罷了)。

Enhancer 是如何和 @observable 裝飾器語法產生聯繫的呢?答案是 @observable 轉義後實際上就是調用 deepDecorator 函數,而該函數須要 deepEnhancer 做爲 「原材料」 才能生成的,仍是以 藥物膠囊 爲例來理解,@observable 就是一個殼,起到運輸包裝做用,真正起做用的仍舊是裏面的 Enhancer

Enhancer 真正起做用地方,是在於通過一路的閉包轉換沉澱,最終會 以參數的方式 傳遞給 new Observable 這個構造函數中,影響所生成的觀察值。

本章所講的內容稍微枯燥一些,也並不是是 MobX 幾大核心概念(Reaction、Observable、ComputedValue),然而所講的裝飾器知識一方面是理解 @ 語法,另外一方面也更好地闡述 Enhancer 的概念,這些都是爲了給後續要講的觀察值(Observable)打基礎。並且通過這一篇文章的講解,你能夠充分體會到裝飾器的概念是如此地深刻到 MobX 體系中,已儼然成爲 MobX 體系中不可分割的一部分。

下面的是個人公衆號二維碼圖片,歡迎關注,及時獲取最新技術文章。
微信公衆號

相關文章
相關標籤/搜索