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

溫馨提示:因微信中外鏈都無法點擊,請通過文末的”  “閱讀原文”  到技術博客中完整查閱版;(本文整理自技術博客)      
  • 初衷:以系列故事的方式展現源碼邏輯,儘可能以易懂的方式講解 MobX 源碼;

  • 本系列文章

  • 《【用故事解讀 MobX源碼(一)】 autorun》

  • 《【用故事解讀 MobX源碼(二)】 computed》

  • 《【用故事解讀 MobX源碼(三)】 shouldCompute》

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

  • 《【用故事解讀 MobX 源碼(五)】 Observable》

  • 文章編排:每篇文章分成兩大段,第一大段以簡單的偵探系列故事的形式講解(所涉及人物、場景都以 MobX 中的概念爲原型創建),第二大段則是相對於的源碼講解。

  • 本文基於 MobX 4 源碼講解

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

所以這是一篇 “插隊” 的文章,用於去理解 MobX 中的裝飾器和 Enhancer 概念。

A. 本文目標

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

  • 在官方文檔 如何(不)使用裝飾器 中,爲什麼說開啓 @observable、@computer 等裝飾器語法,是和直接使用 decorate 是等效的?

  • 在 MobX 源碼中時常出現的 Enhancer 到底是個什麼概念?它在 MobX 體系中發揮怎樣的作用?它和裝飾器又是怎麼樣的一層關係?

如果你也有這樣的疑惑,不妨繼續閱讀本文,歡迎一起討論。

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

1const person = observable({
2    firstName"Clive Staples",
3    lastName"Lewis"
4});
5person.firstName = "C.S.";
6
7const temperature = observable.box(20);
8temperature.set(25);

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

B. 學會裝飾器

1、裝飾器基礎知識

和其他語言(Python、Java)一樣,裝飾器語法是藉助 @ 符號實現的,現在問題就歸結到如何用 JS 去實現 @ 語法。

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

通過裝備特殊的裝備就能將普通人變成鋼鐵俠

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

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

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

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

道理大家都懂,那麼到底如何才能自己動手去實現 @ 裝飾器語法呢?

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

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

1Object.defineProperty(obj, prop, descriptor)

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

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

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

比如 數據描述符

 1Object.getOwnPropertyDescriptor(user,'name');
2
3// 輸出
4/**
5{
6  "value": "張三",
7  "writable": true,
8  "enumerable": true,
9  "configurable": true
10}
11**/

還有 訪問器描述符

 1var anim = { 
2  get age() { return 5; } 
3};
4Object.getOwnPropertyDescriptor(anim, "age");
5// 輸出
6/**
7{
8   configurable: true,
9   enumerable: true,
10   get: /*the getter function*/
,
11   setundefined
12 }
13**/
14

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

接下來,我們一起來看一下 babel 中到底是如何實現 @ 語法的?

3、搭建裝飾器的 babel 示例

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

就拿 MobX 官方的示例 來講:

 1import { observable, computed, action } from "mobx";
2
3class OrderLine {
4    @observable price = 0;
5    @observable amount = 1;
6
7    @computed get total() {
8        return this.price * this.amount;
9    }
10
11    @action.bound
12    increment() {
13        this.amount++ // 'this' 永遠都是正確的
14    }
15}

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

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

4、有兩種方式看轉換之後的代碼

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

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

使用 babel 在線工具查看轉換後的代碼

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

點擊 Add Plugin 按鈕

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

代碼轉換後的效果

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

這是因爲 Babel 只是將最新的 ES6 語法“翻譯”成各大瀏覽器支持比較好的 ES5 語法,但模塊化寫法(require語句)本身就不是 ECMAScript 的標準,而是產生了其他的模塊化寫法標準,例如 CommonJS,AMD,UMD。因此 Babel 轉碼模塊化寫法後在瀏覽器中還是無法運行,此時可以考慮放到 Webpack 這種自動化構建工具環境中,此時 Webpack 是支持模塊化寫法的

如果有強迫症的同學,非得想要這段代碼運行起來,可以參考下述的 方法二

4.2、方法二,使用 demo 工程

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

1npm install
2npm start

說明:package.json 中的 dependencies 字段比較陳舊了,可以自己手動更新到最新版本

打開控制檯就可以看到 bundle.js 文件了:

這樣,我們就可以直接在 index.js 中粘貼我們需要的代碼,因爲基於 Webpack 打包,所以示例代碼是可以運行的。

5、分析轉換之後的代碼邏輯

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

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

1@observable price = 0;

經過 babel 轉譯之後:

 1var _descriptor = _applyDecoratedDescriptor(
2    _class.prototype,
3    'price',
4    [_mobx.observable],
5    {
6      enumerabletrue,
7      initializerfunction initializer() {
8        return 0;
9      }
10    }
11  )

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

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

經過 babel 轉譯之後則爲:

1_applyDecoratedDescriptor(
2  _class.prototype,
3  'total',
4  [_mobx.computed],
5  Object.getOwnPropertyDescriptor(_class.prototype, 'total'),
6  _class.prototype
7);
關鍵的 _applyDecoratedDescriptor 函數

可以看到關鍵是使用了 _applyDecoratedDescriptor 方法。接下來我們着重分析這個方法。

6、關鍵是 `_applyDecoratedDescriptor` 方法

該函數簽名爲:

1function _applyDecoratedDescriptor(
2  target,
3  property,
4  decorators,
5  descriptor,
6  context
7
)
8

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

  • target_class.prototype ,即 OrderLine.prototype

  • property:即字符串 "price"

  • decorators:在這裏是 [_mobx.observable](不同的修飾符裝飾器是不一樣的,比如使用 @computed 修飾的 total 方法,就是 [_mobx.computed]),是長度爲 1 的數組,具體的 observable 方法將在下一篇文章詳細講,就是 createObservable

  • descriptor:即屬性描述符,屬性成員(比如 price)會有 initializer 屬性,而方法成員(比如 total) 則不會有這個屬性,用這個來區分這兩種不同屬性描述符。

1{
2  enumerabletrue,
3  initializerfunction initializer() {
4    return 0;
5  }
6}
  • context:就是運行上下文,一般來講對數據屬性的裝飾則爲 null,對方法屬性則是 _class.prototype

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

_applyDecoratedDescriptor 函數內容

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

  • 圖中標註 ① ,表示返回的 desc 變量就是我們熟悉的 屬性描述符。因此,該 _applyDecoratedDescriptor 的作用就是根據入參返回具體的描述符。

  • 如果是屬性成員(比如price),就將返回的描述符就可以傳給 _initDefineProp (相當於 Object.defineProperty)應用到原來的屬性中去了,從而起到了 裝飾 作用。

    如果是屬性成員,則調用 _initDefineProp 方法
  • 圖中標註 ② ,表示對於方法成員(比如 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 轉譯,那該如何實現類似於上述裝飾器的語法呢?

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

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

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

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

 1import { observable, computed, decorate, action } from "mobx";
2
3class OrderLine {
4    price = 0;
5    amount = 1;
6
7    get total() {
8        return this.price * this.amount;
9    }
10}
11decorate(OrderLine, {
12    price: observable,
13    amount: observable,
14    total: computed,
15    increment: action.bound
16})

3.x 以前的版本因爲沒有 decorate 方法,所以是藉助 extendObservable 方法實現的,具體見文檔 在ES5、ES6和ES.next環境下使用 MobX

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

1decorate(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

1、Enhancer 概念

Enhancer 這個概念是 MobX 自己提出的一個概念,剛接觸到的用戶大多數會先蒙圈一會兒。

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

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

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

2、Enhancer 和 `@observable` 的整體關係

MobX 不是有很多種裝飾器麼,比如 @observable@compute@action,注意 Enhancer 只和 @observable 有關係,和 @compute@action 是沒啥關係的。這是因爲 Enhancer 是爲觀察值(observable)服務的,和計算值(computedValue)和動作(Action)沒關係。

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

形象類比 @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 真正起作用是在什麼地方?

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

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

返回看上面示例中:

1@observable price = 0;

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

我們看一下 observable 源碼 :

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

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

因此上面示例中轉義後的代碼相當於:

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

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

createObservable方法內根據第二個參數進行不同的處理
  • 如果第二個參數不是 string 類型,會走圖中所示 ① 的邏輯,相當於 轉換函數,將普通屬性轉換成 Observable 對象;這部分邏輯我們下一篇文章會着重講到,這裏暫且略過;

  • 如果第二個參數是 string 類型 ,那麼就是本文所述起到 裝飾器 作用,此時方法第二個入參必須是 string,從而會調用 deepDecorator.apply(null, arguments),這是我們這篇文章要繼續講的內容。

探究一下 deepDecorator 的來歷:

1const deepDecorator = createDecoratorForEnhancer(deepEnhancer)

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

直接用 Enhancer 就能創建到對應的裝飾器了

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

4、Enhancer 真正起作用是在什麼地方?

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

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

用一副簡單的調用順序圖來理解 createDecoratorForEnhancer 源碼
  • createDecoratorForEnhancer 裏面會調用 createPropDecorator

  • createPropDecorator 方法執行的時候會調用 defineObservableProperty 方法,createPropDecorator 是一個閉包,所以 defineObservableProperty 能在作用域中獲知 enhancer 變量

  • defineObservableProperty 中會繼續調用 new ObservableValue 創建觀察值,創建的過程中會將 enhancer 作爲參數傳遞進去。

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

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

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

1this.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 體系中不可分割的一部分。


           
END


長按以下二維碼關注,及時獲取最新技術文章

微信中外鏈無法點擊,完整版請點擊下方的"閱讀原文"  


本文分享自微信公衆號 - JSCON簡時空(iJSCON)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章