摘要:最簡單的情況張三的存貸這里我們創建了實例探長實例觀察員這個示例和我們之前在首篇文章用故事解讀源碼一中所用示例是一致的。
================前言===================
初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式講解源碼;
本系列文章:
《【用故事解讀 MobX源碼(一)】 autorun》
《【用故事解讀 MobX源碼(二)】 computed》
《【用故事解讀 MobX源碼(三)】 shouldCompute》
《【用故事解讀 MobX 源碼(四)】裝飾器 和 Enhancer》
《【用故事解讀 MobX 源碼(五)】 Observable》
文章編排:每篇文章分成兩大段,第一大段以簡單的偵探系列故事的形式講解(所涉及人物、場景都以 MobX 中的概念為原型創建),第二大段則是源碼講解。
本文基于 MobX 4 源碼講解
=======================================
A. Story Time寧靜的早上,執行官 MobX 將自己的計算性能優化機制報告呈現給警署最高長官。
在這份報告解說中,談及部署成本最高的地方是在執行任務部分。因此優化這部分任務執行機制,也就相當于優化性能。
警署最高長官瀏覽了報告前言部分,大致總結以下 2 點核心思想:
有兩組人會涉及到任務的執行:執行組(探長) 和 計算組(會計師)
言外之意,觀察組(觀察員)不在優化機制里,他們的行為仍舊按部就班,該匯報的時候就匯報,該提供數據的時候提供數據。
由于執行任務的比較消耗資源,因此執行人員對每一次任務的執行都要問一個”為什么“,最核心的一點是:如果下級人員的數據不是最新的時候,上級人員就不應該執行任務。
那么,執行人員依據什么樣的規則來決定是否執行呢?
警署最高長官繼續往下閱讀,找到了解答該問題的詳細解說。簡言之,為了解決該問題執行官 MobX 給出了狀態調整策略,并在這套策略之上指定的任務執行規則。
由于專業性較強,行文解釋里多處使用代碼。為了更生動形象地解釋這套行為規范,執行官 MobX 在報告里采用 示例 + 圖示 的方式給出生動形象的解釋。
接下來我們在 B. Source Code Time 部分詳細闡述這份 任務執行規則 的內容。
B. Source Code Time執行人員(探長和會計師)依據什么樣的規則來決定是否執行呢?
答案是,執行官 MobX 提供了一個名為 shouldCompute 的方法,每次執行人員(探長和會計師)需要執行之前都要調用該方法 —— 只有該方法返回 true 的時候才會執行任務(或計算)。
在源碼里搜索一下關鍵字 shouldCompute,就可以知道的確只有 derivation(執行組,探長也屬于執行組)、reaction(探長)、computeValue(會計師)這些有執行權力的人才能調用這個方法,而 observerable(觀察員)并不在其中。
也就說 shouldCompute 就是任務執行規則,任務執行規則就是 shouldCompute。而背后支撐 shouldCompute 的則是一套 狀態調整策略
1、狀態調整策略 1.1、L 屬性 和 D 屬性翻開 shouldCompute 源碼, 將會看到 dependenciesState 屬性。
其實這個 dependenciesState(以下簡稱 D 屬性) 屬性還存在一個”孿生“屬性lowestObserverState (以下簡稱 L 屬性)。這兩個屬性正是執行官 MobX 狀態調整策略的核心。
L 屬性 和 D 屬性反映當前對象所處的狀態, 都是枚舉值,且取值區間都是一致的,只能是以下 4 個值之一:
-1: 即 NOT_TRACKING,表示不在調整環節內(還未進入調整調整,或者已經退出調整環節)
0:即 UP_TO_DATE,表示狀態很穩定
1: 即 POSSIBLY_STALE,表示狀態有可能不穩定
2:即 STALE,表示狀態不穩定
上面的文字表述比較枯燥,我們來張圖感受一下:
我們以 “階梯” 來表示上述的狀態值;
UP_TO_DATE(0) 是地面(表示“非常穩定”)
POSSIBLY_STALE(1) 是第一個臺階
STALE(2) 是第 2 個臺階,
NOT_TRACKING(-1)則到地下一層去了
所謂 “高處不勝寒”,距離地面越高,就代表越不穩定。
狀態值 UP_TO_DATE(0)代表的含義是 穩定的狀態,是每個對象所傾向的狀態值。
1.2、調整策略依托L 屬性 和 D 屬性,執行官 MobX 的調整策略應運而生:
只有在 觀察值發生變化 的時候(比如修改了 bankUser.income 屬性值),才會啟用這套機制;
下級成員擁有 L 屬性;而上級成員擁有 D 屬性,比如:
觀察員 O1 只擁有 L 屬性
探長 R1 只擁有 D 屬性
會計師 C1 既擁有 L 屬性,也擁有 D 屬性
某下級成員調整屬性時,調整的策略必須要滿足:自身的 D 屬性 永遠不大于(≤)上級的 L 屬性
某上級成員調整屬性時,調整的策略必須要滿足:其下級成員的 D 屬性 永遠不大于(≤)自身的 L 屬性
觀察值的變更會讓成員的屬性值 上升(提高不穩定性),MobX 執行任務會讓成員屬性值 降低(不穩定性降低);
上述調整策略給我們的直觀感受,就是外界的影響導致 MobX 執行官的部署系統不穩定性上升,為了消除這些不穩定,MobX 會盡可能協調各方去執行任務,從而消除這些個不穩定性。
(舉個不甚恰當的例子,參考人類的免疫機制,病毒感冒后體溫上升就是典型的免疫機制激活的外在表現,抵御完病毒之后體溫又回歸正常)
我們知道,只有上級成員(探長或者設計師)才有執行任務的權力;而一旦滿足上面的調整策略,在任何時刻,執行官 MobX 直接查閱該上級成員的 D 屬性 就能斷定該上級成員(探長或者設計師)是否需要執行任務了,非常簡單方便。
執行官 MobX 判斷的依據都體現在 shouldCompute 方法中了。
本人竊認為這個 shouldCompute 函數的名字太過于抽象,如果讓我命名的話,我更傾向于使用 shouldExecuteTask 這個單詞。
依托L 屬性 和 D 屬性,執行任務規則(即 shouldCompute)就出爐了:
如果屬性值為 NOT_TRACKING(-1)或者 STALE(2),說明自己所依賴的下級數值陳舊了,是時候該重新執行任務(或重新計算)了;
如果屬性值為 UP_TO_DATE(0),說明所依賴的下級的數值沒有更改,是穩定的,不需要重新執行任務。
如果屬性值為 POSSIBLY_STALE(1),說明所依賴的值(一定是計算值,只有計算值的參與才會出現這種狀態)有可能變更,需要讓下級先確認完后再做進一步判斷。這種情況可能不太好理解,后文會詳細說明。
執行任務規則看上去比較簡單,但應用到執行官 MobX 自動化部署方案中情況就復雜了。下面將通過 3 個場景,從簡單到復雜,一步一步來演示L 屬性和D 屬性 是如何巧妙地融合到已有的部署方案中,并以最小的成本實現性能優化的。
2.1、最簡單的情況var bankUser = mobx.observable({ income: 3, debit: 2 }); mobx.autorun(() => { console.log("張三的存貸:", income); }); bankUser.income = 4;
這里我們創建了 autorun 實例 (探長 R1)、observable實例(觀察員O1)
這個示例和我們之前在首篇文章《【用故事解讀 MobX源碼(一)】 autorun》中所用示例是一致的。
當執行 bankUser.income = 4; 語句的時候,觀察員 O1 觀察到的數值變化直接上報給探長 R1,然后探長就執行任務了。關系簡單:
從代碼層面上來講,該 響應鏈 上的關鍵函數執行順序如下:
(O1) reportChange -> (O1) propagateChanged -> (R1) onBecomeStale -> (R1) trackDerivedFunction -> fn(即執行 autorun 中的回調)
其中涉及到 L、D屬性 更改的函數有 propagateChanged 和 track 這兩個。
Step 1:在 propagateChanged 方法執行時,讓觀察員 O1 的 L 屬性 從 0 → 2 ,按照上述的調整原則,探長 R1 的 D屬性 必須要高于觀察員 O1 的 L 屬性,所以其值也只能用從 0 → 2。
Step 2:而隨著 trackDerivedFunction 方法的執行(即探長執行任務)后,觀察員 O1 的 L 屬性 又從 2 → 0,同時也讓探長 R1 的 D屬性 從 2 → 0;
在這里我們已經可以明顯感受到 非穩態的上升 和 削減 這兩個階段:
非穩態的上升:外界更改 bankUser.income 屬性,觸發 propagateChanged 方法,從而讓觀察員的 L 屬性 以及探長的 D屬性 都變成了 2 ,這是系統趨向不穩定的表現。從 層級上來看,是自下而上的過程。
非穩態的削減:隨著變更的傳遞,將觸發探長 R1 的 onBecameStale 方法。執行期間 MobX 執行官查閱探長的 D屬性 是 2,依據 shouldCompute 中的執行規定,同意讓探長執行任務。執行完之后,觀察員的 L 屬性、探長的 D屬性 都下降為 0,表示系統又重新回到穩定狀態。從 層級上來看,是自上而下的過程。
2.2、有單個會計師的情況上面介紹了最簡單的情況,只有一個探長 R1(autorun)和一個觀察員 O1(income)。
現在我們將環境稍微弄復雜一些,新增一個 會計師 C1(divisor) ,此時再來看看上述的變更原則是如何在系統運轉時起作用的:
var bankUser = mobx.observable({ income: 3, debit: 2 }); var divisor = mobx.computed(() => { return bankUser.income / bankUser.debit; }); mobx.autorun(() => { console.log("張三的 divisor:", divisor); }); bankUser.income = 4;
這個示例和我們之前在首篇文章《【用故事解讀 MobX源碼(二)】 computed 》中所用示例是一致的。
當我們執行 bankUser.income = 4; 語句的時候,觀察員 O1 先上報給會計師 C1,接著會計師 C1 會重新執行計算任務后,上報給探長,探長R1 再重新執行任務。
上面描述起來比較簡單,但從代碼層面上來講還是有些繞,先列出該 響應鏈 上的關鍵函數執行順序如下(很明顯比上面的示例要稍微復雜一些):
(O1) reportChange -> (O1) propagateChanged -> (C1) propagateMaybeChanged -> (R1) onBecomeStale(這里并不會讓探長 `runReaction`) -> (O1) endBatch -> (R1) runReaction(到這里才讓探長執行 `runReaction`) -> (C1) reportObserved -> (C1) shouldCompute -> (C1) trackAndCompute -> (C1) propagateChangeConfirmed -> (R1) trackDerivedFunction -> fn(即執行 autorun 中的回調)
注:這里還需要啰嗦一句,雖然這里會觸發探長 R1 的 onBecomeStale 方法,但 MobX 并不會直接讓探長執行任務,這也是 MobX 優化的一種手段體現,詳細分析請移步《【用故事解讀 MobX源碼(二)】 computed 》。
Step 1:在 propagateChanged 方法執行時,讓觀察員 O1 的 L 屬性 從 -1 → 2 ,按照上述的調整原則,其直接上級 C1 的 D屬性 必須要高于觀察員 O1 的 L 屬性,所以其值也只能用從 0 → 2;
和上述簡單示例中最大的不同,在于該期間還涉及到會計師 C1 的狀態更改,具體表現就是調用 propagateMaybeChanged ,在該方法執行后讓會計師 C1 的 L 屬性 從 0 → 1 ,其直接上級 R1 的 D屬性 必須要高于會計師 C1 的 L 屬性,所以其值也從 0 → 1;
注:雖然觀察員 O1 的狀態更改 不能直接 觸發探長 R1 的狀態更改,卻可以憑借會計師 C1 間接 地讓 探長 R1 的狀態發生更改。
Step 2:此步驟是以 會計師 狀態變更為中心演變過程,上一個案例并不存在會計師,所以并不會有該步驟。通過 trackAndCompute 方法,會計師 C1 的 D 屬性 又從 2 → 0,同時也讓觀察員 O1 的 L屬性 從 2 → 0;這個過程表明會計師 C1 的計算值已經更新了。
隨后在 propagateChangeConfirmed 中讓探長 R1 的 D 屬性 從 1 (下級數值可能有更新)→ 2 (確定下級數值確定有更新),同時也讓會計師 C1 的 L 屬性 從 1(告知上級自己的值可能有更新)→ 2 (告知上級自己的值的確有更新);表明探長 R1 和 會計師 C1 的穩態還未達成,需要 Step 3 的執行去消除非穩態。
Step 3:會計師的計算值 C1 更新完畢之后,探長才執行任務。通過 trackDerivedFunction 方法的執行(即探長執行任務)后,會計師 C1 的 L 屬性 又從 2 → 0,同時也讓探長 R1 的 D 屬性 從 2 → 0;
雖然這個示例中,狀態的變更比上面的示例要復雜一些,不過我們依然可以從整體上感受到 非穩態的上升 和 削減 這兩個階段:
非穩態的上升:外界更改 bankUser.income 屬性,觸發 propagateChanged 方法,從而讓觀察員 O1 的 L 屬性 以及會計師 C1 的 D屬性 都變成了 2 ,同時讓會計師 C1 的 L 屬性 以及探長 R1 的 D屬性 都變成了 1 。這是系統趨向不穩定的表現。從 層級上來看,是自下而上的過程。
非穩態的削減:隨著變更的傳遞,有兩次削減非穩態的手段: ① 讓會計師 C1 重新計算; ② 讓探長執行任務。這兩個階段結束之后,所有成員的屬性都下降為 0,表示系統又重新回到穩定狀態。從 層級上來看,是自上而下的過程。
2.3、有兩個會計師的情況我們繼續在上一個示例上修改,再新增一個計算值 indication(這個變量的創建沒有特殊的含義,純粹是為了做演示),由會計師 C2 了負責其進行計算。
var bankUser = mobx.observable({ income: 3, debit: 2 }); var divisor = mobx.computed(() => { return bankUser.income / bankUser.debit; }); var indication = mobx.computed(() => { return divisor / (bankUser.income + 1); }); mobx.autorun(() => { console.log("張三的 indication", indication); }); bankUser.debit = 4;
大體成員和之前的示例相差不大,只是這次我們修改 bankUser.debit 變量(前面兩個示例都是修改 bankUser.income)。
這么做的目的是為了營造出下述的 響應鏈 結構,我們通過修改 bankUser.debit 變量,從而影響 會計師 C1,繼而影響 會計師 C2,最終讓探長 R1 執行任務。
同樣的,我們從代碼層面上來列出該 響應鏈 上的關鍵函數執行順序,比上兩個示例都復雜些,大致如下:
(O2) reportChange -> (O2) propagateChanged -> (C1) propagateMaybeChanged -> (C2) propagateMaybeChanged -> (R1) onBecomeStale(這里并不會讓探長 `runReaction`) -> (O2) endBatch -> (R1) runReaction(到這里才讓探長執行 `runReaction`) -> (R1) shouldCompute -> (C2) shouldCompute -> (C1) shouldCompute -> (C1) trackAndCompute -> (C1) propagateChangeConfirmed -> (C2) trackAndCompute -> (C2) propagateChangeConfirmed -> trackDerivedFunction -> fn(即執行 autorun 中的回調)
Step 1:在 propagateChanged 方法執行時,讓觀察員 O1 的 L 屬性 從 0 → 2 ,按照上述的調整原則,其直接上級 C1 的 D屬性 必須要高于觀察員 O1 的 L 屬性,所以其值也只能用從 0 → 2;
該期間還涉及到會計師 C1、C2 的狀態更改,具體表現就是調用 propagateMaybeChanged ,在該方法執行后讓會計師 C1、C2 的 L 屬性 從 0 → 1 ,他們各自的直接上級 C2、 R1 的 D屬性 值也從 0 → 1;
描述起來比較復雜,其實無非就是多了一個 會計師 C2 的 propagateMaybeChanged 方法過程,一圖勝千言:
Step 2:此步驟是以 會計師 狀態變更為中心演變過程,該步驟是上一個示例中 Step 2 的“復數”版,多個人參與就復雜些,不過條理還是清晰明了的。上個示例中只有一個會計師,所以 trackAndCompute ->propagateChangeConfirmed 的過程只有一次,而這里有兩個會計師,所以這個過程就有兩次(下圖中兩個藍框);
經過該步驟之后會計師 O2、C1 的 L 屬性 又從 2 → 0,同時也讓C1、C2 的 D 屬性 從 2 → 0;這個過程表明觀察員 O1 和 會計師 C1 的計算值已經更新,達到穩態。
而 C2 的 L 屬性 、探長 R1 的 D 屬性 又從 0 → 2,表明探長 R1 和 會計師 C2 的穩態還未達成,需要 Step 3 的執行去消除非穩態。
Step 3:探長執行任務,通過 trackDerivedFunction 方法的執行(即探長執行任務)后,會計師 C2 的 L 屬性 又從 2 → 0,同時也讓探長 R1 的 D 屬性 從 2 → 0;這一步和上個示例中的 Step 3 幾乎相同。
在這個示例中,狀態的變更縱使比上面的示例要復雜得多,但我們還是很清晰地從整體上感受到 非穩態的上升 和 削減 這兩個階段:
非穩態的上升:外界更改 bankUser.debit 屬性,觸發 propagateChanged 方法,從而讓觀察員 O1 開始,依次影響 會計師 C1、C2,以及探長 R1 的 L、D 屬性從 0 變成 1 或者 2,這是系統趨向不穩定的表現。從 層級上來看,是自下而上的過程。
非穩態的削減:隨著變更的傳遞,有兩次削減非穩態的手段: ① 讓會計師 C1 、C2 重新計算; ② 讓探長 R1 執行任務。這兩個階段結束之后,所有成員的屬性都下降為 0,表示系統又重新回到穩定狀態。從 層級上來看,是自上而下的過程。
2.4、一點點總結通過上面三個從簡單逐步到復雜的示例,我們簡單總結歸納一下 MobX 在處理狀態變更過程中所采取執行機制以及其背后的調整策略:
先是自下而上傳遞非穩態:這是一個自下而上的過程,由觀察員發起這個過程,在這個過程中依次將外界的變更層層向上傳遞,改變每個相關成員的 L、D屬性。 這個期間會拒絕一切成員任務執行的申請(比如探長執行任務、會計師執行計算任務等等)。
其次自上而下消解非穩態:這是一個自上而下的過程。當非穩態到達頂層后,由頂層人員(一般是探長類)開始做決策執行任務,在執行任務中凡是遇到有非穩態的成員(比如會計師、觀察員),責令他們更新狀態,消除非穩態,逐層逐層地消除非穩態。等整個任務執行完之后,每個成員都處于穩態狀態,開始下一個變更的到來。
3、狀態圖在軟件設計中,為了更好地顯示這種狀態變更和事件之間的關系,常常使用 狀態圖 來展現(沒錯,就是 UML建模中的那個狀態圖)
如果不太熟悉,這里給個參考文章 UML建模之狀態圖(Statechart Diagram) 方便查閱。
挨個總結上述 3 個案例中 L、D屬性,我們將其中的事件和屬性改變抽離出來,就能獲取狀態圖了,方便我們從另外一個角度理解和體會。
3.1、L 屬性Observable(觀察員)、ComputeValue(會計師)這兩種類型擁有 L 屬性 :
3.2、D 屬性Reaction(探長)、ComputeValue(會計師)這兩種類型擁有 D 屬性:
所以,會計師同時擁有 L屬性 和 D 屬性4、小測試
如果我們將 2.3、有兩個會計師的情況 示例中的 bankUser.debit = 4; 修改成 bankUser.income = 6; 的話,那各個成員對象的 D 屬性、L 屬性 的變化情況又是怎么樣的?
5、本文總結如何在復雜的場景下兼顧計算性能?
MobX 提供了 shouldCompute 方法用于直接判斷是否執行計算(或任務),判斷的依據非常簡單,只要根據對象的 dependenciesState 屬性是否為 true 就能直接作出判斷。
而其背后的支持則是 dependenciesState 屬性(上文中的 D 屬性)和 lowestObserverState (上文中的 L 屬性),這兩個屬性依托 MobX 中自動化機制在適當時機(搭”順風車“)進行變更。因此,無論多么復雜的場景下 MobX 能以低廉的成本兼顧性能方面的治理,充分運用惰性求值思想減少計算開銷。
初看 MobX 源碼,它往往給你一種 ”雜項叢生“的感覺(調試這段代碼的時候真是心里苦啊),但其實在這背后運轉著一套清晰的 非穩態傳遞 和 非穩態削減 的固定模式,一旦掌握這套模式之后,MobX 自動化響應體系的脈絡已清晰可見,這將為你更好理解 MobX 的運行機制打下扎實的基礎。
到本篇為止,我們已經耗費 3 篇文章來解釋 MobX 的(絕大部分)自動化響應機制。經過這 3 篇文章,讀者應該對 MobX 的整個運轉機制有了一個比較清晰明了的理解。后續的文章中將逐漸縮減”故事“成分,將講解重心轉移到 MobX 本身概念(比如 Observable、decorator、Atom等)源碼的解讀上,相信有了這三篇文章的作為打底,理解其余部分更多的是在語法層面,閱讀起來將更加游刃有余。
下面的是我的公眾號二維碼圖片,歡迎關注,及時獲取最新技術文章。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/94720.html
摘要:隨后,執行官給出一張當張三存款發生變化之時,此機構的運作時序圖的確,小機構靠人力運作,大機構才靠制度運轉。第一條語句創建觀察員第一條語句張三我們調用的時候,就創建了對象,對象的所有屬性都將被拷貝至一個克隆對象并將克隆對象轉變成可觀察的。 ================前言=================== 初衷:網上已有很多關于 MobX 源碼解讀的文章,但大多閱讀成本甚高。...
摘要:場景為了多維度掌控嫌疑犯的犯罪特征數據,你警署最高長官想要獲取并實時監控張三的貸款數額存貸比存款和貸款兩者比率的變化。 ================前言=================== 初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式講解源碼; 本系列文章: 《【用故事解讀 MobX源碼(一)】 autorun》 《【用故事解讀 MobX源碼(二)】...
摘要:前言初衷以系列故事的方式展現源碼邏輯,盡可能以易懂的方式講解源碼本系列文章用故事解讀源碼一用故事解讀源碼二用故事解讀源碼三用故事解讀源碼四裝飾器和用故事解讀源碼五文章編排每篇文章分成兩大段,第一大段以簡單的偵探系列故事的形式講解所涉及人物場 ================前言=================== 初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式...
摘要:所以這是一篇插隊的文章,用于去理解中的裝飾器和概念。因此,該的作用就是根據入參返回具體的描述符。其次局部來看,裝飾器具體應用表達式是,其函數簽名和是一模一樣。等裝飾器語法,是和直接使用是等效等價的。 ================前言=================== 初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式講解源碼; 本系列文章: 《【用故事解...
摘要:源碼簡記整體會寫得比較亂,同時也比較簡單,和讀書筆記差不多,基本是邊讀邊寫。見諒主要三大部分的原子類,能夠被觀察和通知變化,繼承于。同時里面有幾個比較重要的屬性與方法。 Mobx 源碼簡記 整體會寫得比較亂,同時也比較簡單,和讀書筆記差不多,基本是邊讀邊寫。見諒~ 主要三大部分Atom、Observable、Derivation Atom Mobx的原子類,能夠被觀察和通知變化,obs...
閱讀 2111·2021-11-23 10:06
閱讀 3477·2021-11-11 16:54
閱讀 3343·2019-08-29 17:31
閱讀 3569·2019-08-29 17:05
閱讀 2171·2019-08-26 13:36
閱讀 2159·2019-08-26 12:17
閱讀 525·2019-08-26 12:12
閱讀 1673·2019-08-26 10:19