摘要:所以這是一篇插隊的文章,用于去理解中的裝飾器和概念。因此,該的作用就是根據入參返回具體的描述符。其次局部來看,裝飾器具體應用表達式是,其函數簽名和是一模一樣。等裝飾器語法,是和直接使用是等效等價的。
================前言===================
初衷:以系列故事的方式展現 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 的用法就足夠了,比如(示例摘自官方文檔):
const person = observable({ firstName: "Clive Staples", lastName: "Lewis" }); person.firstName = "C.S."; const temperature = observable.box(20); temperature.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 方法實現裝飾器設計模式,該方法簽名如下:
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 中到底是如何實現 @ 語法的?
3、搭建裝飾器的 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、有兩種方式看轉換之后的代碼 4.1、 方法一,使用 babel 在線工具放到 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 是支持模塊化寫法的
如果有強迫癥的同學,非得想要這段代碼運行起來,可以參考下述的 方法二。
4.2、方法二,使用 demo 工程官方提供了 mobx-react-boilerplate,clone 下來之后直接:
npm install npm start
說明:package.json 中的 dependencies 字段比較陳舊了,可以自己手動更新到最新版本
打開控制臺就可以看到 bundle.js 文件了:
這樣,我們就可以直接在 index.js 中粘貼我們需要的代碼,因為基于 Webpack 打包,所以示例代碼是可以運行的。
5、分析轉換之后的代碼邏輯上述兩種方法因為都是使用同一個裝飾器轉換插件 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 方法。接下來我們著重分析這個方法。
6、關鍵是 _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;
看完函數簽名,我們繼續看函數內容:
這幾行代碼沒啥難度,就是我們熟悉的 屬性描述符 相關的內容:
圖中標注 ① ,表示返回的 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 插件對于裝飾器語法 @ 所做的事情:
通過 ast 分析,將 @ 語法轉換成 _applyDecoratedDescriptor 方法的應用
_applyDecoratedDescriptor 方法就是一個循環應用裝飾器的過程
那么接下來我們回到主題,mobx 如果不使用 babel 轉譯,那該如何實現類似于上述裝飾器的語法呢?
7、不用裝飾器語法,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 方法實現的,具體見文檔 在ES5、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 是什么概念?
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 裝飾器有效的那部分。可以用 "藥物膠囊
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95931.html
摘要:前言初衷以系列故事的方式展現源碼邏輯,盡可能以易懂的方式講解源碼本系列文章用故事解讀源碼一用故事解讀源碼二用故事解讀源碼三用故事解讀源碼四裝飾器和用故事解讀源碼五文章編排每篇文章分成兩大段,第一大段以簡單的偵探系列故事的形式講解所涉及人物場 ================前言=================== 初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式...
摘要:場景為了多維度掌控嫌疑犯的犯罪特征數據,你警署最高長官想要獲取并實時監控張三的貸款數額存貸比存款和貸款兩者比率的變化。 ================前言=================== 初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式講解源碼; 本系列文章: 《【用故事解讀 MobX源碼(一)】 autorun》 《【用故事解讀 MobX源碼(二)】...
摘要:最簡單的情況張三的存貸這里我們創建了實例探長實例觀察員這個示例和我們之前在首篇文章用故事解讀源碼一中所用示例是一致的。 ================前言=================== 初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式講解源碼; 本系列文章: 《【用故事解讀 MobX源碼(一)】 autorun》 《【用故事解讀 MobX源碼(二)】...
摘要:隨后,執行官給出一張當張三存款發生變化之時,此機構的運作時序圖的確,小機構靠人力運作,大機構才靠制度運轉。第一條語句創建觀察員第一條語句張三我們調用的時候,就創建了對象,對象的所有屬性都將被拷貝至一個克隆對象并將克隆對象轉變成可觀察的。 ================前言=================== 初衷:網上已有很多關于 MobX 源碼解讀的文章,但大多閱讀成本甚高。...
摘要:此時為空對象當構造函數原型對象上存在屬性,則執行下面代碼。屬性名傳遞進來的函數描述符構造函數原型對象此時為最后通過調用函數為屬性生成描述符。初始值獲取之后,調用方法,傳入構造函數原型對象屬性名初始值和。 observable 同時支持 decorator 方式和方法調用方式。 // 示例 @observable name = 張三; const temp = observable.bo...
閱讀 1825·2019-08-30 15:55
閱讀 1020·2019-08-26 11:57
閱讀 523·2019-08-26 11:29
閱讀 3367·2019-08-26 10:49
閱讀 1920·2019-08-23 18:40
閱讀 1826·2019-08-23 16:04
閱讀 3117·2019-08-23 11:01
閱讀 2285·2019-08-23 10:56