================前言===================html
本系列文章:react
=======================================git
按照步驟,這篇文章應該寫 觀察值(Observable)的,不過在撰寫的過程當中發現,若是不先搞明白裝飾器和 Enhancer(對這個單詞陌生的,先不要着急,繼續往下看) ,直接去解釋觀察值(Observable)會很費勁。由於在 MobX 中是使用裝飾器設計模式實現觀察值的,因此說要先掌握裝飾器,才能進一步去理解觀察值。es6
因此這是一篇 「插隊」 的文章,用於去理解 MobX 中的裝飾器和 Enhancer 概念。github
本文主要解決我我的在源碼閱讀中的疑惑:npm
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
和其餘語言(Python、Java)同樣,裝飾器語法是藉助 @
符號實現的,如今問題就歸結到如何用 JS 去實現 @
語法。設計模式
對於還不熟悉裝飾器語法的讀者,這裏推薦文章 《ES7 Decorator 裝飾者模式》,以鋼鐵俠爲例,經過裝備特殊的裝備就能將普通人變成鋼鐵俠,簡單歸納起來就是:
裝飾器設計模式的理念就和上面那樣的樸素,在不改造 託尼·史塔克(Tony Stark) 本體的前提下,經過加裝 盔甲、飛行器 的方式加強 Tony 的能力,從而「變成」鋼鐵俠。
有關裝飾器使用的文章,還能夠參考這兩篇參考文章 探尋 ECMAScript 中的裝飾器 Decorator、細說ES7 JavaScript Decorators
文章都比較早,當時寫文章的做者都認爲在新的 ES7 裏會推出標準的 @
語法,然而過後證實官方並無這個意願。咱們知道目前的 ECMAScript 2015 標準,甚至到 ECMAScript 2018 標準官方都沒有提供 @
語法的支持,咱們在其餘文章中看到的 @
語法都是經過 babel 插件來實現的。
上面說起的參考文章都是屬於應用類型的,就是直接使用裝飾器語法(即直接使用 @
語法)來展現裝飾器的實際應用,而對於如何實現 @
語法並無說起 —— 那就是如何用 Object.defineProperty 來實現 @
語法。
道理你們都懂,那麼到底如何才能本身動手去實現 @
裝飾器語法呢?
在 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 對於裝飾器 @
語法的內部實現了。
就拿 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。
放到 babel 在線工具,粘貼現有的示例代碼會報錯,不過 babel 給出了友好的提示,由於使用到了裝飾器語法,須要安裝 babel-plugin-transform-decorators-legacy:
咱們點擊左下方的 Add Plugin 按鈕,在彈出的搜索框裏輸入關鍵字 decorators-legacy,選擇這個插件就能夠:
選完插件以後,代碼就會成功轉譯:
底下會提示 require is not defined 錯誤,這個錯誤並不影響你分析裝飾器的語法,由於有 @
符號部分都已經轉換成 ES5 語法了,只是這個報錯沒法讓這段示例代碼運行起來。
這是由於 Babel 只是將最新的 ES6 語法「翻譯」成各大瀏覽器支持比較好的 ES5 語法,但模塊化寫法(
require
語句)自己就不是 ECMAScript 的標準,而是產生了其餘的模塊化寫法標準,例如 CommonJS,AMD,UMD。所以 Babel 轉碼模塊化寫法後在瀏覽器中仍是沒法運行,此時能夠考慮放到 Webpack 這種自動化構建工具環境中,此時 Webpack 是支持模塊化寫法的
若是有強迫症的同窗,非得想要這段代碼運行起來,能夠參考下述的 方法二。
官方提供了 mobx-react-boilerplate,clone 下來以後直接:
npm install npm start
說明:package.json 中的
dependencies
字段比較陳舊了,能夠本身手動更新到最新版本
打開控制檯就能夠看到 bundle.js 文件了:
這樣,咱們就能夠直接在 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 );
能夠看到關鍵是使用了 _applyDecoratedDescriptor
方法。接下來咱們着重分析這個方法。
_applyDecoratedDescriptor
方法該函數簽名爲:
function _applyDecoratedDescriptor( target, property, decorators, descriptor, context )
具體的用法,以 price
屬性爲例,咱們能夠獲取對應的實參:
_class.prototype
,即 OrderLine.prototype
"price"
[_mobx.observable]
(不一樣的修飾符裝飾器是不同的,好比使用 @computed
修飾的 total
方法,就是 [_mobx.computed]
),是長度爲 1 的數組,具體的 observable
方法將在下一篇文章詳細講,就是 createObservable price
)會有 initializer
屬性,而方法成員(好比 total
) 則不會有這個屬性,用這個來區分這兩種不一樣屬性描述符。{ enumerable: true, initializer: function initializer() { return 0; } }
null
,對方法屬性則是 _class.prototype
;看完函數簽名,咱們繼續看函數內容:
這幾行代碼沒啥難度,就是咱們熟悉的 屬性描述符 相關的內容:
desc
變量就是咱們熟悉的 屬性描述符。所以,該 _applyDecoratedDescriptor
的做用就是根據入參返回具體的描述符。price
),就將返回的描述符就能夠傳給 _initDefineProp
(至關於 Object.defineProperty
)應用到原來的屬性中去了,從而起到了 裝飾 做用。
total
)則直接應用 Object.defineProperty
方法(當是方法成員時,desc
是沒有 initializer
屬性的),同時令 desc = null,從後續的應用來看並不會和 _initDefineProp
方法搭配使用對於圖中標註 ③ ,咱們具體看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 插件對於裝飾器語法 @
所作的事情:
@
語法轉換成 _applyDecoratedDescriptor
方法的應用_applyDecoratedDescriptor
方法就是一個循環應用裝飾器的過程那麼接下來咱們回到主題,mobx 若是不使用 babel 轉譯,那該如何實現相似於上述裝飾器的語法呢?
很顯然,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 就是具體的裝飾器函數(好比 observable
、computed
和 action.bound
這樣具體的裝飾器有效函數)摘出核心語句:
能夠看去的確就是一個 for
循環,而後依次應用 decorator
,這剛好就是 babel 插件轉換後 _applyDecoratedDescriptor
方法所作的事情,所以二者是等效的。
這樣,就解答了本文開篇提出的第一個疑問。 @observable、@computer 等裝飾器語法,是和直接使用 decorate 是等效等價的。
看到這裏是否是以爲有點兒難以想象?嗯,事實上裝飾器應用的過程就這麼的簡單。你也能夠直接將這個 decorate API 方法直接提取到本身的項目中使用,給你的項目增長新的 feature。
解答完第一個問題,咱們繼續講本文開頭提出的另外一個問題:MobX 中的 enhancer
是什麼概念?
Enhancer 這個概念是 MobX 本身提出的一個概念,剛接觸到的用戶大多數會先蒙圈一下子。
學習過 MobX 3.x 及之前版本的人可能會遇到 Modifier 這個概念,Enhancer
其實就是 Modifier
。
Modifier 在 MobX 3 以前的版本里官方有專門的 文檔 解說。不過到 MobX 4.x 以後官方就刪除了這篇文檔。好在這個概念是內部使用的,修更名字對外部調用者沒有啥影響。
Enhancer
從字面上理解是 加強器,其做用就是給原有的對象 增長額外的功能 —— 這不就是裝飾器的做用麼?沒錯,它是輔助 MobX 中的 @observable
裝飾器功能的。結合裝飾器,會更加容易理解這個概念。
@observable
的總體關係MobX 不是有不少種裝飾器麼,好比 @observable
、@compute
和 @action
,注意 Enhancer
只和 @observable
有關係,和 @compute
和 @action
是沒啥關係的。這是由於 Enhancer
是爲觀察值(observable)服務的,和計算值(computedValue)和動做(Action)不要緊。
@observable
裝飾器中真正起做用的函數就是 Enhancer ,你能夠將 Enhancer 理解成 @observable
裝飾器有效的那部分。能夠用 "藥物膠囊💊" 來理解 @observable
裝飾器和 Enhancer 的關係:
@observable
裝飾器就像是膠囊的外殼,內裏攜帶的藥物成分就是 Enhancer,由於真正起效果的部分是 Enhancer @observable
裝飾器僅僅是起到包裝、傳輸到指定目的地的做用。@observable
相關的代碼,頂可能是不能使用裝飾器功能而已。@observable
裝飾器是這種狀況,其餘的裝飾器(包括 @compute
和 @action
這樣的裝飾器以及本身寫的裝飾器)都不在此討論範疇 在 MobX 中有 4 種 Enhancer,在 types/modifier.ts 中有定義:
不理解的話能夠參考 Mobx 源碼解讀(三) Modifier 文章,有詳細的示例解說,本文就不展開了。
接下來,咱們須要解決的是有兩個問題:
@observable
裝飾器語法產生聯繫的?@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 類型 而起到不一樣的做用:
string
,從而會調用 deepDecorator.apply(null, arguments)
,這是咱們這篇文章要繼續講的內容。探究一下 deepDecorator
的來歷:
const deepDecorator = createDecoratorForEnhancer(deepEnhancer)
經過給 createDecoratorForEnhancer 方法傳入 deepEnhancer
就能夠了。從這個 createDecoratorForEnhancer 方法的名字就能知道其含義,基於 enhancer 建立裝飾器,是否是有點神奇,直接用 Enhancer 就能建立到對應的裝飾器了!MobX 中其餘 enhancer 也是基於這個函數建立相應的裝飾器的:
這個過程就是 @observable
裝飾器語法 和 enhancer 產生聯繫的地方。
繼續研究 createDecoratorForEnhancer
方法就能探知 Enhancer 起做用的地方。
不過接下來的函數分解,涉及到各類閉包來回整,很容易把人繞暈。這裏作了一副簡單的調用順序圖:
createDecoratorForEnhancer
裏面會調用 createPropDecorator
和createPropDecorator
方法執行的時候會調用 defineObservableProperty
方法,createPropDecorator
是一個閉包,因此 defineObservableProperty
能在做用域中獲知 enhancer
變量defineObservableProperty
中會繼續調用 new ObservableValue
建立觀察值,建立的過程當中會將 enhancer
做爲參數傳遞進去。這裏就不展開講解,看得很暈也不用在乎,有個大概瞭解就行。感興趣的讀者,能夠挨個在源碼中查找上述的函數名字,感覺他們互相調用的關係,外加再看一下 defineObservableProperty 源碼就能夠。
下一篇文章着重分析觀察值(Observable)過程的時候,還會涉及這部分邏輯,這裏咱們知道大體的結論就行:最終的 enhancer
會傳遞給 ObservableValue
構造函數,從而影響觀察值建立過程。
具體的影響在 ObservableValue
的構造函數中就體現出來,直接影響觀察值對象中的 value
屬性:
this.value = enhancer(value, undefined, name)
再結合 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 體系中不可分割的一部分。
下面的是個人公衆號二維碼圖片,歡迎關注,及時獲取最新技術文章。