摘要:前言初衷以系列故事的方式展現源碼邏輯,盡可能以易懂的方式講解源碼本系列文章用故事解讀源碼一用故事解讀源碼二用故事解讀源碼三用故事解讀源碼四裝飾器和用故事解讀源碼五文章編排每篇文章分成兩大段,第一大段以簡單的偵探系列故事的形式講解所涉及人物場
================前言===================
初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式講解源碼;
本系列文章:
《【用故事解讀 MobX源碼(一)】 autorun》
《【用故事解讀 MobX源碼(二)】 computed》
《【用故事解讀 MobX源碼(三)】 shouldCompute》
《【用故事解讀 MobX 源碼(四)】裝飾器 和 Enhancer》
《【用故事解讀 MobX 源碼(五)】 Observable》
文章編排:每篇文章分成兩大段,第一大段以簡單的偵探系列故事的形式講解(所涉及人物、場景都以 MobX 中的概念為原型創建),第二大段則是源碼講解。
本文基于 MobX 4 源碼講解
=======================================
A. Story Time最高警長看完執行官(MobX)的自動部署方案,對 “觀察員” 這個基層人員工作比較感興趣,自執行官拿給他部署方案的時候,他就注意到所有上層人員的功能都是基于該底層人員高效的工作機制;
第二天,他找上執行官(MobX)一起去視察“觀察員”所在機構部門(下面簡稱為 ”觀察局“),想更深入地了解 “觀察員” 運行分配機制。
當最高警長到達部門的時候,恰好遇到該部門恰好要開始執行 MobX 前不久新下發的任務,要求監控 parent 對象的一舉一動:
var parent = { child: { name: "tony", age: 15 } name: "john" } var bankUser = observable(parent);
任務達到觀察局辦公室后,相應的辦公室文員會對任務進行分析,然后會依據對象類型交給相應科室進行處理,常見的有 object 科,另外還有 map 科和 array 科;
現在,辦公室文員見傳入的對象是 parent 是個對象,就將其傳遞給 object 科,讓其組織起一起針對該 parent 對象的 ”觀察小組“,組名為 bankUser。
object 科接到任務,委派某位科長(以下稱為 bankUser 科長)組成專項負責此 parent 對象的觀察工作,bankUser 科長接手任務后發現有兩個屬性,其中 child 是對象類型,age 是原始值類型,就分別將任務委派給 child 小科長 和 name 觀察員 O1,child 小科長接到任務后再委派給 name 觀察員 O2 和 age 觀察員 O3,最終執行該任務的人員結構如下:
觀察員的任務職責我們已經很熟悉了,當讀寫觀察員對應的數據時將觸發 reportObserved 或 propagateChanged 方法;
這里涉及到兩位科長(bankUser 科長 和 child 小科長),那么科長的任務職責是什么呢?
科長的人物職責是起到 管理 作用,它負責統管在他名下的觀察員。比如當我們讀寫 bankUser.child 對象的 name 屬性時(比如執行語句 bankUser.child.name = "Jack"),首先感知到讀寫操作的并非是 觀察員 O2 而是bankUser科長,bankUser科長會告知 child 小科長有數據變更,child 小科長然后再將信息傳達給 name 觀察員 O2 ,然后才是觀察員 O2 對數據讀寫起反應,這才讓觀察員 O2 發揮作用。
從代碼層面看,我們看到僅僅是執行 bankUser.child.name = "Jack"這一行語句,和我們平常修改對象屬性并無二致。然而在這一行代碼背后其實牽動了一系列的操作。這其實是 MobX 構建起的一套 ”鏡像“ 系統,使用者仍舊按平時的方式讀寫對象,然而每個屬性的讀寫操作實則都鏡像到觀察局 的某個小組具體的操作;非常類似于古代的 ”垂簾聽政“ ,看似皇帝坐在文武百官前面,其實真正做出決策響應的是藏在簾后面的那個人。
前幾章中我們只看到觀察員在活動,然則背后離不開 科長 這一角色機制在背后暗暗的調度。對每項任務,最終都會落實到觀察員采取“一對一”模式監控分配到給自己的觀察項,而每個觀察員肯定是隸屬于某個 ”科長“ 帶領。在 MobX 系統里,辦公室、科長和觀察員是密不可分,共同構建起 觀察局 運行體制;
"分工明確,運轉高效",這是最高警長在巡視完觀察員培訓基地后的第一印象,觀察局運轉的每一步的設計都有精細的考量;
B. Source Code Time先羅列本文故事中人物與 MobX 源碼概念映射關系:
故事人物 | MobX 源碼 | 解釋 |
---|---|---|
警署最高長官 | (無) | MobX 用戶,沒錯,就是你 |
執行官 MobX | MobX | 整個 MobX 運行環境 |
觀察局辦公室(主任、文員) | observable、observable.box 等 | 用于創建 Observable 的 API |
object 科室、map 科室、array 科室 | observable.object、observable.map、observable.array | 將不同復合類型轉換成觀察值的方法 |
科長 | ObservableObjectAdministration | 主要給對象添加 $mobx 屬性 |
觀察員 | ObservableValue 實例 | ObservableValue 實例 |
observable 對應上述故事中的 觀察局辦公室主任 角色,本身不提供轉換功能,主要是起到統一調度作用 —— 這樣 MobX 執行官只需要將命令發給辦公室人員就行,至于內部具體的操作、具體由哪個科室處理,MobX 執行官不需要關心。
將與 observable 的源碼 相關的源碼稍微整理,就是如下的形式:
var observable = createObservable; // 使用“奇怪”的方式來擴展 observable 函數的功能,就是將 observableFactories 的方法挨個拷貝給 observable Object.keys(observableFactories).forEach(function(name) { return (observable[name] = observableFactories[name]); });
首先 observable 是函數,函數內容就是 createObservable
其次 observable 是對象,對象屬性和 observableFactories 一致
也就是說 observable 其實是 各種構造器的總和,整合了 createObservable(默認構造器) + observableFactories(其他構造器)
自己也可以在 console 控制臺中打印來驗證一番:
const { observable } = mobx; console.log("observable name:", observable.name); console.log(Object.getOwnPropertyNames(observable));
從以下控制臺輸出的結果來看,observable 的屬性的確來自于createObservable 和 observableFactories 這兩者:
文字比較枯燥,用圖來表示就是下面那樣子:
這里我大致劃分了一下,分成 4 部分內容來理解:
第一部分:createObservable 方法剛才粗略講過,是 MobX API 的 observable 的別名,是一個高度封裝的方法,算是一個總入口,方便用戶調用;該部分對應上述故事中的 觀察局辦公室主任 的角色
第二部分:box 是一個轉換函數,用于將 原值(primitive value) 直接轉換成 ObservableValue 對象;shallowBox 是 box 函數的非 deep 版本;該部分對應上述故事中的 觀察局辦公室文員 的角色;
第三部分:針對 object、array 以及 map 這三種數據類型分別提供轉換函數,同時也提供 shallow 的版本;該部分對應上述故事中的 科室 部分;
第四部分:提供四種裝飾器函數,裝飾器的概念我們上一節課講過,主要輔助提供裝飾器語法糖作用;對普通 MobX 用戶來講這部分平時也是接觸不到的;
如何理解這 4 部分的之前的關系呢?我個人的理解如下:
第三部分屬于 “基層建筑”,分別為 object、array 以及 map 這三種數據類型提供轉換成可觀察值的功能(默認是遞歸轉換,shallow 表示非遞歸轉換);這部分對應上述故事中的科室概念,不同的觀察任務由不同的科室來處理;
第一部分和第二部分屬于 “上層建筑”,提供統一的接口,具體的轉換功能都是調用第三部分中的某個轉換函數來實現的;這兩部分對應上述故事中的 觀察局辦公室 部分。
第一部分我們最熟悉,不過第二部分的 box 函數轉換能力反而比第一部分更廣,支持將原始值轉換成可觀察值;
第四部分和另外三部分沒有直接的關系,主要輔助提供裝飾器函數;注意,沒有直接的聯系并不代表沒有聯系,第四部分中裝飾器內的核心邏輯和另外三部分是一樣的(比如都調用 decorator 方法)。
下面我們看兩個具體的示例,來輔助消化上面的結論。
示例一:observable.box(obj) 底層就是調用 observable.object(obj)實現的
var user = { income: 3, name: "張三" }; var bankUser = observable.object(user); var bankUser2 = observable.box(user); console.log(bankUser); console.log(bankUser2);
可以發現 bankUser2 中的 value 屬性部分內容和 bankUser 是一模一樣的。
示例二:observable.box(primitive) 能行,observable(primitive) 卻會報錯
var pr1 = observable.box(2); console.log(pr1); console.log("--------華麗分割-----------") var pr2 = observable(2); console.log(pr2);
從報錯信息來看,MobX 會友情提示你改用 observable.box 方法實現原始值轉換:
2、第一部分:createObservable正如上面所言,該函數其實就是 MobX API 的 observable 的 “別名”。所以也是對應上述故事中的 觀察局辦公室主任 角色;
該函數本身不提供轉換功能,只是起到 "轉發" 作用,將傳入的對象轉發給對應具體的轉換函數就行了;
看一下 源碼,
function createObservable(v, arg2, arg3) { // 走向 ① if (typeof arguments[1] === "string") { return deepDecorator.apply(null, arguments); } // 走向 ② if (isObservable(v)) return v; var res = isPlainObject(v) ? observable.object(v, arg2, arg3) // 走向③ : Array.isArray(v) ? observable.array(v, arg2) // 走向 ④ : isES6Map(v) ? observable.map(v, arg2) // 走向 ⑤ : v; if (res !== v) return res; // 走向 ⑥ fail( process.env.NODE_ENV !== "production" && `The provided value could not be converted into an observable. If you want just create an observable reference to the object use "observable.box(value)"` ) }
不難看出其實是典型的采用了 策略設計模式 ,將多種數據類型(Object、Array、Map)情況的轉換封裝起來,好讓調用者不需要關心實現細節:
該設計模式參考可參考 深入理解JavaScript系列(33):設計模式之策略模式
用圖來展示一下具體的走向:
走向 ① 是 裝飾器語法所特有的,這是因為此時傳給 createObservable 的第二個參數是 string 類型,這一點我們在上一篇文章有詳細論述;
走向 ② 很直觀,如果傳入的參數就已經是 觀察值 了,不多廢話直接返回傳入的值,不需要轉換;
走向 ③、④ 、⑤ 是直根據傳入參數的類型分別調用具針對具體類型的轉換方法;
走向 ⑥,在上面示例中我們已經看到過, 針對原始值會提示建議用戶使用 observable.box 方法。
第一部分的 createObservable 的內容就那么些,總之只是起了 “向導” 作用。是不是比你想象中的要簡單?
接下來我們繼續看第二部分的 observable.box 方法。
3、第二部分:observable.box這個方法對應上述故事中的 觀察局辦公室文員 角色,也是屬于辦公室部門的,所起到的作用和 主任 大同小異,只是平時我們用得并不多罷了。
當我第一次閱讀 官網文檔 中針對有關 observable.box 的描述時:
來回讀了幾次,“盒子”是個啥?它干嘛用的? “observable” 和 “盒子” 有半毛錢關系?
直到看完該函數的詳細介紹 boxed values 后,方才有所感悟,這里這 box 方法就是將普通函數 “包裝” 成可觀察值,所以 box 是動詞而非名詞 。
準確地理解,observable.box 是一個轉換函數,比如我們將普通的原始值 "Pekin"(北京)轉換成可觀察值,就可以使用:
const cityName = observable.box("Pekin");
原始值 "Pekin" 并不具備可觀察屬性,而經過 box 方法操作之后的 cityName 變量具有可觀察性,比如:
console.log(cityName.get()); // 輸出 "Pekin" cityName.observe(function(change) { console.log(change.oldValue, "->", change.newValue); }); cityName.set("Shanghai"); // 輸出 "Pekin -> Shanghai"
從輸入輸出角度來看,這 box 其實就是將普通對象轉換成可觀察值的過程,轉換過程中將一系列能力“添加”到對象上,從而獲得 “自動響應數值變化” 的能力。
那么具體這 box 函數是如何實現的呢?直接看 源碼。
box: function(value, options) { if (arguments.length > 2) incorrectlyUsedAsDecorator("box"); var o = asCreateObservableOptions(options); return new ObservableValue( value, getEnhancerFromOptions(o), o.name ); }
發現該方法僅僅是調用 ObservableValue 構造函數,所以 box 方法操作的結果是返回 ObservableValue 實例。
這里的 asCreateObservableOptions 方法僅僅是格式化入參 options 對象而已。4、核心類:ObservableValue
總算是講到這個 ObservableValue 類了,該類是理解可觀察值的關鍵概念。這個類對應上述故事中的 觀察員 角色,就是最基層的 name 觀察員 O1、O2、O3 那些。
本篇文章的最終目的也就是為了講清楚這個 ObservableValue 類,其他的概念反而是圍繞它而創建起來的。
分析其源碼,將這個類的屬性和方法都拎出來瞧瞧,繪制成類圖大致如下:
你會發現該類 繼承自 Atom 類,所以在理解 ObservableValue 之前必須理解 Atom。
其實在 3.x 版本的時候,ObservableValue 繼承自 BaseAtom;4.1、Atom
隨著升級到 4.x 版本,官方以及廢棄了 BaseAtom,直接繼承自 Atom 這個類。
在 MobX 的世界中,任何能夠 存儲并管理 狀態的對象都是 Atom,故事中的 觀察員(ObservableValue 實例)本質上就是 Atom(準確的說,而 ObservableValue 是繼承了 Atom 這個基類),Atom實例有兩項重大的使命:
當它的值被使用的時候,就會觸發 reportObserved 方法,在 第一篇文章 的講解中可知,MobX 正是基于該方法,使得觀察員和探長之間建立關聯關系。
當它的值受到更改的時候,將會觸發 reportChanged 方法,在第三篇文章 《【用故事解讀 MobX源碼(三)】 shouldCompute》中可知,基于該方法觀察員就可以將 非穩態信息逐層上傳,最終將讓探長、會計員重新執行任務。
Atom 類圖如下,從中我們看到前面幾章中所涉及到的 onBecomeUnobserved、onBecomeObserved、reportObserved、reportChanged 這幾個核心方法,它們都來源于 Atom 這個類:
所以說 Atom 是整個 MobX 的基石并不為過,所有的自動化響應機制都是建立在這個最最基礎類之上。正如在大自然中,萬物都是由原子(atom)構成的,借此意義, MobX 中的 ”具備響應式的“ 對象都是由這個 Atom 類構成的。
(ComputeValue類 也繼承自 Atom,Reaction 類的實現得依靠 Atom,因此不難感知 Atom 基礎重要性)
理論上你只要創建一個 Atom 實例就能融入到 mobx 的響應式系統中,
如何自己創建一個 Atom 呢?
MobX 已經暴露了一個名為 createAtom 方法,
官方文檔 創建 observable 數據結構和 reactions(反應) 給出了創建一個 鬧鐘 的例子,具體講解了該 createAtom 方法的使用:
... // 創建 atom 就能和 MobX 核心算法交互 this.atom = createAtom( // 第一個參數是 name 屬性,方便后續 "Clock", // 第二個參數是回調函數,可選,當 atom 從 unoberved 狀態轉變到 observed () => this.startTicking(), // 第三個參數也是回調函數,可選,與第二個參數對應,此回調是當 atom 從 oberved 狀態轉變到 unobserved 時會被調用 // 注意到,同一個 atom 有可能會在 oberved 狀態和 unobserved 之間多次轉換,所以這兩個回調有可能會多次被調用 () => this.stopTicking() ); ...
同時文中也給出了對應的最佳實踐:
最好給創建的 Atom 起一個名字,方便后續 debug
onBecomeObserved 和 onBecomeUnobserved 和我們面向對象中構造函數與析構函數的作用相似,方便進行資源的申請和釋放。
不過 Atom 實例這個還是偏向底層實現層,除非需要強自定義的特殊場景中,平時我們推薦直接使用 observable 或者 observable.box 來創建觀察值更為簡單直接;
4.3、理解 ObservableValueMobX 在 Atom 類基礎上,泛化出一個名為 ObservableValue 類,就是我們耳熟能詳的 觀察值 了。從代碼層面上來看,實現 ObservableValue 其實就是繼承一下 Atom 這個類,然后再添加許多輔助的方法和屬性就可以了。
理解完上述的 Atom 對象之后,你就已經理解 ObservableValue 的大部分。接下來就是去理解 ObservableValue 相比 Atom 多出來的屬性和方法,我這里并不會全講,太枯燥了。只挑選重要的兩部分 —— Intercept & Observe 部分 和 enhancer 部分
4.3.1、Intercept & Observe 部分在 ObservableValue 類圖中除了常見的 toJSON()、toString() 方法之外,有兩個方法格外引人注目 —— intercept() 和 observe 兩個方法。
如果把 “對象變更” 作為事件,那么我們可以在 事件發生之前 和 事件方法之后 這兩個 “切面” 分別可以安插回調函數(callback),方便程序動態擴展,這屬于 面向切面編程的思想。
不了解 AOP 的,可以查閱 知乎問答-什么是面向切面編程AOP?
在 MobX 世界里,將安插在 事件發生之前 的回調函數稱為 intercept,將安插在 事件發生之后 的回調函數稱為 observe。理解這兩個方法可以去看 官方中的示例,能快速體會其作用。
這里稍微進一步講細致一些,有時候官方文檔會中把 intercept 理解成 攔截器。 這是因為它作用于事件(數據變更)發生之前,因此可以操縱變更的數據內容,甚至可以通過返回 null 忽略某次數據變化而不讓它生效。
其作用機制也很直接,該方法調用的最終都是調用實例的 intercept 方法,這樣每次在值變更之前(以下 prepareNewValue 方法執行),都會觸發觀察值上所綁定的所有的 攔截器:
ObservableValue.prototype.prepareNewValue = function(newValue) { ... if (hasInterceptors(this)) { var change = interceptChange(this, { object: this, type: "update", newValue: newValue }); if (!change) return UNCHANGED; newValue = change.newValue; } // apply modifier ... };
著重里面的那行語句 if (!change) return UNCHANGED; ,如果你在 intercept 安插的回調中返回 null 的話,相當于告知 MobX 數值沒有變更(UNCHANGED),既然值沒有變更,后續的邏輯就不會觸發了。
observe 的作用是將回調函數安插在值變更之后(以下 setNewValue 方法調用),同樣是通過調用 notifyListeners 通知所有的監聽器:
ObservableValue.prototype.setNewValue = function(newValue) { ... this.reportChanged(); if (hasListeners(this)) { notifyListeners(this, { type: "update", object: this, newValue: newValue, oldValue: oldValue }); } };
==========【以下是額外的知識內容,可跳過,不影響主線講解】===========
如何解除安插的回調函數?
Intercept & Observe 這兩個函數返回一個 disposer 函數,這個函數是 解綁函數,調用該函數就可以取消攔截器或者監聽器 了。這里有一個最佳實踐,如果不需要某個攔截器或者監聽器了,記得要及時清理自己綁定的監聽函數 永遠要清理 reaction —— 即調用 disposer 函數。
那么如何實現 disposer 解綁函數這套機制?
以攔截器(intercept)為例,注冊的時候調用 registerInterceptor 方法:
function registerInterceptor(interceptable, handler) { var interceptors = interceptable.interceptors || (interceptable.interceptors = []); interceptors.push(handler); return once(function() { var idx = interceptors.indexOf(handler); if (idx !== -1) interceptors.splice(idx, 1); }); }
整體的邏輯比較清晰,就是將傳入的 handler(攔截器)添加到 interceptors 數組屬性中。關鍵是在于返回值,返回的是一個閉包 —— once 函數調用的結果值。
所以我們簡化一下 disposer 解綁函數的定義:
disposer = once(function() { var idx = interceptors.indexOf(handler); if (idx !== -1) interceptors.splice(idx, 1); });
恰是這個 once 函數是實現解綁功能的核心。
查看這個 once 函數源碼只有寥寥幾行,卻將閉包的精髓運用到恰到好處。
function once(func) { var invoked = false; return function() { if (invoked) return; invoked = true; return func.apply(this, arguments); }; }
該 once 方法其實通過 invoked 變量,控制傳入的 func 函數只調用一次。
回過頭來 disposer 解綁函數,調用一次就會從 interceptors 數組中移除當前攔截器。使用 once 函數后,你無論調用多少次 disposer 方法,最終都只會解綁一次。
由于 once 是純函數,因此大伙兒可以提取出來運用到自己的代碼庫中 —— 這也是源碼閱讀的益處之一,借鑒源碼中優秀部分,然后學習吸收,引以為用。
=======================================================
4.3.2、enhancer 部分這部分是在 ObservableValue 構造函數中發揮作用的,其影響的恰恰是最核心的數據屬性:
function ObservableValue(value, enhancer, name, notifySpy) { ... _this.enhancer = enhancer; _this.value = enhancer(value, undefined, name); ... }
在上一篇文章《【用故事解讀 MobX 源碼(四)】裝飾器 和 Enhancer》中有提及過 enhance,在那里我們說起過 enhance 其實就是裝飾器(decorator)的有效成分,該有效成分影響的正是本節所講的 ObservableValue 對象。結合 types/modifier.ts 中有各種 Enhancer 的具體內容,就能大致了解 enhancer 是如何起到 轉換數值 的作用的,以常見的 deepEnhancer 為例,當在構造函數中執行 _this.value = enhancer(value, undefined, name); 的時候會進入到 deepEnhance 函數體內:
function deepEnhancer(v, _, name) { // it is an observable already, done if (isObservable(v)) return v; // something that can be converted and mutated? if (Array.isArray(v)) return observable.array(v, { name: name }); if (isPlainObject(v)) return observable.object(v, undefined, { name: name }); if (isES6Map(v)) return observable.map(v, { name: name }); return v; }
這段代碼是否似曾相識?!沒錯,和上一節所述 createObservable 方法幾乎一樣,采用 策略設計模式 調用不同具體轉換函數(比如 observable.object 等)。
現在應該能夠明白,第一部分的 createObservable 和 第二部分的 observable.box 都是建立在第三部分之上,而且通過第一部分、第二部分以及第三部分獲得的觀察值對象都是屬于觀察值對象(ObservableValue),大同小異,頂多只是“外形”有略微的差別。
通過該 enhancer 部分的講解,我們發現所有待分析的重要部分都聚焦到第三部分的 observable.object 等這些個轉換方法身上了。
5、第三部分:observable.object因為結構的原因,上面先講了最基層的 ObservableValue 部分,現在回來講的 observable.object 方法。從這里你能大概體會到 MobX 體系中遞歸現象: new ObservableValue 里面會調用 observable.object 方法,從后面的講解里你將會看到 observable.object 方法里面也會調用 new ObservableValue 的操作,所以 遞歸地將對象轉換成可觀察值 就很順理成章。
閱讀官方文檔 Observable.object,該 observable.object 方法就是把一個普通的 JavaScript 對象的所有屬性都將被拷貝至一個克隆對象并將克隆對象轉變成可觀察的,而且 observable 是 遞歸應用 的。
observable.object 等方法對應于上述故事中的 科室 部分,用于執行具體的操作。常見的 object 科室是將 plan object 類型數據轉換成可觀察值,map 科室是將 map 類型數據轉換成可觀察值....
我們查閱 observable.object(object) 源碼,其實就 2 行有效代碼:
object: function(props, decorators, options) { if (typeof arguments[1] === "string") incorrectlyUsedAsDecorator("object"); var o = asCreateObservableOptions(options); return extendObservable({}, props, decorators, o); },
可以說 observable.object(object) 實際上是 extendObservable({}, object) 的別名,從這里 extendObservable 方法的第一個參數是 {} 可以看到,最終產生的觀察值對象是基于全新的對象,不影響原始傳入的對象內容。
5.1、extendObservable 方法講到這里,會有一種恍然大悟,原來 extendObservable 方法才是最終大 boss,一切觀察值的創建終歸走到這個函數。查看該方法的 源碼,函數簽名如下:
extendObservable(target, properties, decorators, options)
必須接收 2 ~ 4 個參數
第一個參數必須是對象,比如 bankUser
第二個參數是屬性名,比如 name
第三個參數是 裝飾器 配置項,這一知識點在上一篇章已經講解。
第四個參數是配置選項對象
方法具體的使用說明參考 官方文檔 extendObservable
將該方法的主干找出來:
function extendObservable(target, properties, decorators, options) { ... // 第一步 調用 asObservableObject 方法給 target 添加 $mobx 屬性 options = asCreateObservableOptions(options); var defaultDecorator = options.defaultDecorator || (options.deep === false ? refDecorator : deepDecorator); asObservableObject( target, options.name, defaultDecorator.enhancer ); // 第二步 循環遍歷,將屬性經過 decorator(裝飾器) 改造后添加到 target 上 startBatch(); for (var key in properties) { var descriptor = Object.getOwnPropertyDescriptor( properties, key ); var decorator = decorators && key in decorators ? decorators[key] : descriptor.get ? computedDecorator : defaultDecorator; var resultDescriptor = decorator( target, key, descriptor, true ); if (resultDescriptor){ Object.defineProperty(target, key, resultDescriptor); } } endBatch(); return target;
這方法看上去塊頭很大,不過分析起來就 2 大步:
首先調用 asObservableObject 方法,給 target 生成 $mobx 屬性
其次挨個讓每個屬性經過 decorator 改造后重新安裝到 target 上,默認的 decorator 是 deepDecorator,裝飾器的含義和作用在上一篇文章已講過,點擊 這里 復習
5.2、第一步:調用 asObservableObjectasObservableObject 方法,主要是給目標對象生成 $mobx 屬性;該 $mobx 屬性對應上述故事中的 科長 角色,用于管理對象的讀寫操作。
為什么要添加 $mobx 屬性?其具體作用又是什么?
通過閱讀源碼,我無從獲知作者添加 $mobx 屬性的理由,但可以知道 $mobx 的作用是什么。
首先,$mobx 屬性是一個 ObservableObjectAdministration 對象,類圖如下:
用例子來看看 $mobx 屬性:
var bankUser = observable({ income: 3, name: "張三" }); console.table(bankUser);
下圖紅框處標示出來的就是 bankUser.$mobx 屬性:
我們進一步通過以下兩行代碼輸出 $mobx 屬性中具體的數據成員和擁有的方法成員:
console.log(`bankUser.$mobx:`, bankUser.$mobx); console.log(`bankUser.$mobx.__proto__:`, bankUser.$mobx.__proto__);
在這么多屬性中,格外需要注意的是 write 和 read 這兩個方法,這兩個方法算是 $mobx 屬性的靈魂,下面即將會講到,這里先點名一下。
除此之外還需要關注 $mobx 對象中的 values 屬性,剛初始化的時候該屬性是 {} 空對象,不過注意上面截圖中看到 $mobx.values 是有內容的,這其實不是在這一步完成,而是在接下來要講的第二步中所形成的。
你可以這么理解,這一步僅僅是找到擔任科長的人選,還是光桿司令;下一步才是正式委派科長到某個科室,那個時候新上任的科長才有權力管束其下屬的觀察員。
5.3、第二步:每個屬性都經過一遍 decorator 的 “洗禮”這部分就是應用 裝飾器 操作了,默認是使用 deepDecorator 這個裝飾器。裝飾器的應用流程在 上一篇文章 中有詳細講解,直接拿結論過來:
你會發現應用裝飾器的最后一步是在調用 defineObservableProperty 方法時創建 ObservableValue 屬性,對應在 defineObservableProperty 源碼 中以下語句:
var observable = (adm.values[propName] = new ObservableValue( newValue, enhancer, adm.name + "." + propName, false ));
這里的 adm 就是 $mobx 屬性,這樣新生成的 ObservableValue 實例就掛載在 $mobx.values[propName] 屬性下。
這樣的設定很巧妙,值得我們深挖。先看一下下面的示例:
var user = { income: 3, name: "張三" }; var bankUser = observable(user); bankUser.income = 5; console.log(bankUser.income); console.table(bankUser.$mobx.values.income);
在這個案例中,我們直接修改 bankUser 的 income 屬性為 5,一旦修改,此時 bankUser.$mobx.values.income 也會同步修改:
這是怎么做到的呢?
答案是:通過 generateObservablePropConfig 方法
function generateObservablePropConfig(propName) { return ( observablePropertyConfigs[propName] || (observablePropertyConfigs[propName] = { configurable: true, enumerable: true, get: function() { return this.$mobx.read(this, propName); }, set: function(v) { this.$mobx.write(this, propName, v); } }) ); }
該方法是作用在 decorator 裝飾器其作用期間,用 generateObservablePropConfig 生成的描述符重寫原始對象的描述符,仔細看描述符里的 get 和 set 方法,對象屬性的 讀寫分別映射到 $mobx.read 和 $mobx.write這兩個方法中。
在這里,我們就能知道掛載 $mobx 屬性的意圖:MobX 為我們創建了原對象屬性的 鏡像 操作,所有針對原有屬性的讀寫操作都將鏡像復刻到 $mobx.values 對應 Observable 實例對象上,從而將復雜的操作隱藏起來,給用戶提供直觀簡單的,提高用戶體驗。
以賦值語句 bankUser.income = 5 為例,這樣的賦值語句我們平時經常寫,只不過這里的 bankUser 是我們 observable.object 操作得到的,所以 MobX 會同步修改 bankUser.$mobx.values.income 這個 ObservableValue 實例對象,從而觸發 reportChanged 或者 reportObserved 等方法,開啟 響應式鏈 的第一步。
你所做的操作和以往一樣,書寫 bankUser.income = 5 這樣的語句就可以。而實際上 mobx 在背后默默地做了很多工作,這樣就將簡單的操作留給用戶,而把絕大多數復雜的處理都隱藏給 MobX 框架來處理了。
5.4、遞歸實現觀察值本小節開始已經提及過遞歸傳遞觀察值,這里再從代碼層面看一下 遞歸實現觀察值 的原理。這一步是在 decorator 裝飾器應用過程中,通過 $mobx 掛載對應屬性的 ObservableValue 實例達到的。
對應的操作在剛才的 5.3 已經講過,還是在 defineObservableProperty 源碼 那行代碼:
var observable = (adm.values[propName] = new ObservableValue( newValue, enhancer, adm.name + "." + propName, false ));
以下述的 parent 對象為例:
var parent = { child: { name: "tony" } }
當我們執行 observable(parent)(或者 new ObservableValue(parent) 、 observable.box(parent) 等創建觀察值的方法),其執行路徑如下:
從上圖就可以看到,在 decorator 那一步將屬性轉換成 ObservableValue 實例,這樣在整體上看就是遞歸完成了觀察值的轉換 —— 把 child 和它下屬的屬性也轉換成可觀察值。
6、小測試請分析 observable.map 和 observable.array 的源碼,看看它們和 observable.object 方法之間的差別在哪兒。
7、總結本文重點是講 Observable 類,與之相關的類圖整理如下:
ObservableValue 繼承自 Atom,并實現一系列的 接口;
ObservableObjectAdministration 是 鏡像操作管理者,它主要通過 $mobx 屬性來操控管理每個觀察值 ObservableValue
比較重要的方法是 intercept 和 observe ,用“面向切口”編程的術語來講,這兩個方法就是兩個 切口,分別作用于數值更改前后,方便針對數據狀態做一系列的響應;
本文中出現很多 observable 相關的單詞,稍作總結:
ObservableValue 是一個普通的 class,用于表示 觀察值 這個概念。
observable 是一個函數,也是 mobx 提供的 API,等于 createObservable,代表操作,該操作過程中會根據情況調用 observable.object(或者 observable.array、observable.map)等方法,最終目的是為了創建 ObservableValue 對象。
extendObservable,這是一個工具函數,算是比較底層的方法,該方法用來向已存在的目標對象添加 observable 屬性;上述的 createObservable 方法其實也是借用該方法實現的;
MobX 默認會遞歸將對象轉換成可觀察屬性,這主要是得益于 enhancer 在其中發揮的作用,因為每一次 Observable 構造函數會對傳入的值經過 enhancer 處理;
有人不禁會問,既然提供 observable 方法了,那么 observable.box 方法存在的意義是什么?答案是,由于它直接返回的是 ObservableValue,它相比普通的 observable 創建的觀察值,提供更加細粒度(底層)的操作;
比如它除了能像正常觀察值一樣和 autorun 搭配使用之外,創建的對象還直接擁有 intercept 和 observe 方法:
var pr1 = observable.box(2); autorun(() => { console.log("value:", pr1.get()); }); pr1.observe(change => { console.log("change from", change.oldValue, "to", change.newValue); }); pr1.set(3); // 以下是輸出結果: // value: 2 // value: 3 // change from 2 to 3
當然 MobX 考慮也很周全,還多帶帶提供 Intercept & Observe 兩個工具函數,以函數調用的方式給觀察值新增這兩種回調函數。
因此下述兩種方式是等同的,可以自己試驗一下:
// 調用 observe 屬性方法 pr1.observe(change => { console.log("change from", change.oldValue, "to", change.newValue); }); // 使用 observe 工具函數可以達到相同的目的 observe(pr1, change => { console.log("change from", change.oldValue, "to", change.newValue); }):
本文針對 MobX 4 源碼講解,而在 MobX 5 版本中的 Observable 類則是采用 proxy 來實現 Observable,整體思路和上述的并無二致,只是在細節方面將 Object.defineProperty 替換成 new Proxy 的寫法而已,感興趣的同學建議先閱讀 《抱歉,學會 Proxy 真的可以為所欲為》了解 Proxy 的寫法,然后去看一下 MobX 5 中的 observable.object 方法已經改用 createDynamicObservableObject 來創建 proxy,所創建的 proxy 模型來自于 objectProxyTraps 方法;如有機會將在后續的文章中更新這方面的知識。
用故事講解 MobX 源碼的系列文章至此告一段落,后續以散篇的形式發布跟 MobX 相關的文章。
下面的是我的公眾號二維碼圖片,歡迎關注,及時獲取最新技術文章。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96563.html
摘要:所以這是一篇插隊的文章,用于去理解中的裝飾器和概念。因此,該的作用就是根據入參返回具體的描述符。其次局部來看,裝飾器具體應用表達式是,其函數簽名和是一模一樣。等裝飾器語法,是和直接使用是等效等價的。 ================前言=================== 初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式講解源碼; 本系列文章: 《【用故事解...
摘要:隨后,執行官給出一張當張三存款發生變化之時,此機構的運作時序圖的確,小機構靠人力運作,大機構才靠制度運轉。第一條語句創建觀察員第一條語句張三我們調用的時候,就創建了對象,對象的所有屬性都將被拷貝至一個克隆對象并將克隆對象轉變成可觀察的。 ================前言=================== 初衷:網上已有很多關于 MobX 源碼解讀的文章,但大多閱讀成本甚高。...
摘要:最簡單的情況張三的存貸這里我們創建了實例探長實例觀察員這個示例和我們之前在首篇文章用故事解讀源碼一中所用示例是一致的。 ================前言=================== 初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式講解源碼; 本系列文章: 《【用故事解讀 MobX源碼(一)】 autorun》 《【用故事解讀 MobX源碼(二)】...
摘要:場景為了多維度掌控嫌疑犯的犯罪特征數據,你警署最高長官想要獲取并實時監控張三的貸款數額存貸比存款和貸款兩者比率的變化。 ================前言=================== 初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式講解源碼; 本系列文章: 《【用故事解讀 MobX源碼(一)】 autorun》 《【用故事解讀 MobX源碼(二)】...
摘要:源碼簡記整體會寫得比較亂,同時也比較簡單,和讀書筆記差不多,基本是邊讀邊寫。見諒主要三大部分的原子類,能夠被觀察和通知變化,繼承于。同時里面有幾個比較重要的屬性與方法。 Mobx 源碼簡記 整體會寫得比較亂,同時也比較簡單,和讀書筆記差不多,基本是邊讀邊寫。見諒~ 主要三大部分Atom、Observable、Derivation Atom Mobx的原子類,能夠被觀察和通知變化,obs...
閱讀 749·2021-10-14 09:43
閱讀 2072·2021-09-30 09:48
閱讀 3440·2021-09-08 09:45
閱讀 1090·2021-09-02 15:41
閱讀 1878·2021-08-26 14:15
閱讀 770·2021-08-03 14:04
閱讀 2972·2019-08-30 15:56
閱讀 3072·2019-08-30 15:52