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

================前言===================html

=======================================git

A. Story Time

寧靜的早上,執行官 MobX 將本身的計算性能優化機制報告呈現給警署最高長官。github

在這份報告解說中,談及部署成本最高的地方是在執行任務部分。所以優化這部分任務執行機制,也就至關於優化性能。segmentfault

警署最高長官瀏覽了報告前言部分,大體總結如下 2 點核心思想:性能優化

  • 兩組人會涉及到任務的執行:執行組(探長) 和 計算組(會計師)
言外之意, 觀察組(觀察員)不在優化機制裏,他們的行爲仍舊循序漸進,該彙報的時候就彙報,該提供數據的時候提供數據。
  • 因爲執行任務的比較消耗資源,所以執行人員對每一次任務的執行都要問一個」爲何「,最核心的一點是:若是下級人員的數據不是最新的時候,上級人員就不該該執行任務。

clipboard.png

那麼,執行人員依據什麼樣的規則來決定是否執行呢?微信

警署最高長官繼續往下閱讀,找到了解答該問題的詳細解說。簡言之,爲了解決該問題執行官 MobX 給出了狀態調整策略,並在這套策略之上指定的任務執行規則函數

因爲專業性較強,行文解釋裏多處使用代碼。爲了更生動形象地解釋這套行爲規範,執行官 MobX 在報告裏採用 示例 + 圖示 的方式給出生動形象的解釋。性能

接下來咱們在 B. Source Code Time 部分詳細闡述這份 任務執行規則 的內容。測試

B. Source Code Time

執行人員(探長和會計師)依據什麼樣的規則來決定是否執行呢?

答案是,執行官 MobX 提供了一個名爲 shouldCompute 的方法,每次執行人員(探長和會計師)須要執行以前都要調用該方法 —— 只有該方法返回 true 的時候纔會執行任務(或計算)。

在源碼裏搜索一下關鍵字 shouldCompute,就能夠知道的確只有 derivation(執行組,探長也屬於執行組)、 reaction(探長)、 computeValue(會計師)這些有執行權力的人才能調用這個方法,而 observerable(觀察員)並不在其中。
clipboard.png

也就說 shouldCompute 就是任務執行規則任務執行規則就是 shouldCompute。而背後支撐 shouldCompute 的則是一套 狀態調整策略

一、狀態調整策略

1.一、L 屬性D 屬性

翻開 shouldCompute 源碼, 將會看到 dependenciesState 屬性。

clipboard.png

其實這個 dependenciesState(如下簡稱 D 屬性) 屬性還存在一個」孿生「屬性lowestObserverState (如下簡稱 L 屬性)。這兩個屬性正是執行官 MobX 狀態調整策略的核心。

L 屬性D 屬性反映當前對象所處的狀態, 都是枚舉值,且取值區間都是一致的,只能是如下 4 個值之一:

  • -1: 即 NOT_TRACKING,表示不在調整環節內(還未進入調整調整,或者已經退出調整環節)
  • 0:即 UP_TO_DATE,表示狀態很穩定
  • 1: 即 POSSIBLY_STALE,表示狀態有可能不穩定
  • 2:即 STALE,表示狀態不穩定

上面的文字表述比較枯燥,咱們來張圖感覺一下:

clipboard.png

咱們以 「階梯」 來表示上述的狀態值;

  • UP_TO_DATE(0) 是地面(表示「很是穩定」)
  • POSSIBLY_STALE(1) 是第一個臺階
  • STALE(2) 是第 2 個臺階,
  • NOT_TRACKING(-1)則到地下一層去了
  • 所謂 「高處不勝寒」,距離地面越高,就表明越不穩定
  • 狀態值 UP_TO_DATE(0)表明的含義是 穩定的狀態,是每一個對象所傾向的狀態值。

1.二、調整策略

依託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.一、最簡單的狀況

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,而後探長就執行任務了。關係簡單:

upstream

從代碼層面上來說,該 響應鏈 上的關鍵函數執行順序以下:

(O1) reportChange 
    -> (O1) propagateChanged 
    -> (R1) onBecomeStale 
      -> (R1) trackDerivedFunction 
         -> fn(即執行 autorun 中的回調)

其中涉及到 L、D屬性 更改的函數有 propagateChangedtrack 這兩個。

Step 1:在 propagateChanged 方法執行時,讓觀察員 O1 的 L 屬性 從 0 → 2 ,按照上述的調整原則,探長 R1 的 D屬性 必需要高於觀察員 O1 的 L 屬性,因此其值也只能用從 0 → 2。

pagechagned

Step 2:而隨着 trackDerivedFunction 方法的執行(即探長執行任務)後,觀察員 O1 的 L 屬性 又從 2 → 0,同時也讓探長 R1 的 D屬性 從 2 → 0;

track

在這裏咱們已經能夠明顯感覺到 非穩態的上升削減 這兩個階段:

  • 非穩態的上升:外界更改 bankUser.income 屬性,觸發 propagateChanged 方法,從而讓觀察員的 L 屬性 以及探長的 D屬性 都變成了 2 ,這是系統趨向不穩定的表現。從 層級上來看,是自下而上的過程。
  • 非穩態的削減:隨着變動的傳遞,將觸發探長 R1 的 onBecameStale 方法。執行期間 MobX 執行官查閱探長的 D屬性 是 2,依據 shouldCompute 中的執行規定,贊成讓探長執行任務。執行完以後,觀察員的 L 屬性、探長的 D屬性 都降低爲 0,表示系統又從新回到穩定狀態。從 層級上來看,是自上而下的過程。

2.二、有單個會計師的狀況

上面介紹了最簡單的狀況,只有一個探長 R1(autorun)和一個觀察員 O1(income)。

如今咱們將環境稍微弄複雜一些,新增一個 會計師 C1divisor) ,此時再來看看上述的變動原則是如何在系統運轉時起做用的:

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 再從新執行任務。

c1 upstream

上面描述起來比較簡單,但從代碼層面上來說仍是有些繞,先列出該 響應鏈 上的關鍵函數執行順序以下(很明顯比上面的示例要稍微複雜一些):

(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;

maybechanged

注:雖然觀察員 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 的執行去消除非穩態。

trackAndCompute

Step 3:會計師的計算值 C1 更新完畢以後,探長才執行任務。經過 trackDerivedFunction 方法的執行(即探長執行任務)後,會計師 C1 的 L 屬性 又從 2 → 0,同時也讓探長 R1 的 D 屬性 從 2 → 0;

track

雖然這個示例中,狀態的變動比上面的示例要複雜一些,不過咱們依然能夠從總體上感覺到 非穩態的上升削減 這兩個階段:

  • 非穩態的上升:外界更改 bankUser.income 屬性,觸發 propagateChanged 方法,從而讓觀察員 O1 的 L 屬性 以及會計師 C1 的 D屬性 都變成了 2 ,同時讓會計師 C1 的 L 屬性 以及探長 R1 的 D屬性 都變成了 1 。這是系統趨向不穩定的表現。從 層級上來看,是自下而上的過程。
  • 非穩態的削減:隨着變動的傳遞,有兩次削減非穩態的手段: ① 讓會計師 C1 從新計算; ② 讓探長執行任務。這兩個階段結束以後,全部成員的屬性都降低爲 0,表示系統又從新回到穩定狀態。從 層級上來看,是自上而下的過程。

2.三、有兩個會計師的狀況

咱們繼續在上一個示例上修改,再新增一個計算值 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 執行任務。

two compute

一樣的,咱們從代碼層面上來列出該 響應鏈 上的關鍵函數執行順序,比上兩個示例都複雜些,大體以下:

(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;

該期間還涉及到會計師 C一、C2 的狀態更改,具體表現就是調用 propagateMaybeChanged ,在該方法執行後讓會計師 C一、C2 的 L 屬性 從 0 → 1 ,他們各自的直接上級 C二、 R1 的 D屬性 值也從 0 → 1;

描述起來比較複雜,其實無非就是多了一個 會計師 C2 的 propagateMaybeChanged 方法過程,一圖勝千言:

c2 upstream

Step 2:此步驟是以 會計師 狀態變動爲中心演變過程,該步驟是上一個示例中 Step 2 的「複數」版,多我的參與就複雜些,不過條理仍是清晰明瞭的。上個示例中只有一個會計師,因此 trackAndCompute ->propagateChangeConfirmed 的過程只有一次,而這裏有兩個會計師,因此這個過程就有兩次(下圖中兩個藍框);

c2 compute

通過該步驟以後會計師 O二、C1 的 L 屬性 又從 2 → 0,同時也讓C一、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 幾乎相同。

c2 track

在這個示例中,狀態的變動縱使比上面的示例要複雜得多,但咱們仍是很清晰地從總體上感覺到 非穩態的上升削減 這兩個階段:

  • 非穩態的上升:外界更改 bankUser.debit 屬性,觸發 propagateChanged 方法,從而讓觀察員 O1 開始,依次影響 會計師 C一、C2,以及探長 R1 的 L、D 屬性從 0 變成 1 或者 2,這是系統趨向不穩定的表現。從 層級上來看,是自下而上的過程。
  • 非穩態的削減:隨着變動的傳遞,有兩次削減非穩態的手段: ① 讓會計師 C1 、C2 從新計算; ② 讓探長 R1 執行任務。這兩個階段結束以後,全部成員的屬性都降低爲 0,表示系統又從新回到穩定狀態。從 層級上來看,是自上而下的過程。

2.四、一點點總結

經過上面三個從簡單逐步到複雜的示例,咱們簡單總結概括一下 MobX 在處理狀態變動過程當中所採起執行機制以及其背後的調整策略:

  • 先是自下而上傳遞非穩態:這是一個自下而上的過程,由觀察員發起這個過程,在這個過程當中依次將外界的變動層層向上傳遞,改變每一個相關成員的 L、D屬性。 這個期間會拒絕一切成員任務執行的申請(好比探長執行任務、會計師執行計算任務等等)。
  • 其次自上而下消解非穩態:這是一個自上而下的過程。當非穩態到達頂層後,由頂層人員(通常是探長類)開始作決策執行任務,在執行任務中凡是遇到有非穩態的成員(好比會計師、觀察員),責令他們更新狀態,消除非穩態,逐層逐層地消除非穩態。等整個任務執行完以後,每一個成員都處於穩態狀態,開始下一個變動的到來。

三、狀態圖

在軟件設計中,爲了更好地顯示這種狀態變動和事件之間的關係,經常使用 狀態圖 來展示(沒錯,就是 UML建模中的那個狀態圖)

若是不太熟悉,這裏給個參考文章 UML建模之狀態圖(Statechart Diagram) 方便查閱。

挨個總結上述 3 個案例中 L、D屬性,咱們將其中的事件和屬性改變抽離出來,就能獲取狀態圖了,方便咱們從另一個角度理解和體會。

3.一、L 屬性

Observable(觀察員)、ComputeValue(會計師)這兩種類型擁有 L 屬性

L attr

3.二、D 屬性

Reaction(探長)、ComputeValue(會計師)這兩種類型擁有 D 屬性
D attr

因此,會計師同時擁有 L屬性D 屬性

四、小測試

若是咱們將 2.三、有兩個會計師的狀況 示例中的 bankUser.debit = 4; 修改爲 bankUser.income = 6; 的話,那各個成員對象的 D 屬性L 屬性 的變化狀況又是怎麼樣的?

五、本文總結

如何在複雜的場景下兼顧計算性能?

MobX 提供了 shouldCompute 方法用於直接判斷是否執行計算(或任務),判斷的依據很是簡單,只要根據對象的 dependenciesState 屬性是否爲 true 就能直接做出判斷。

而其背後的支持則是 dependenciesState 屬性(上文中的 D 屬性)和 lowestObserverState (上文中的 L 屬性),這兩個屬性依託 MobX 中自動化機制在適當時機(搭」順風車「)進行變動。所以,不管多麼複雜的場景下 MobX 能以低廉的成本兼顧性能方面的治理,充分運用惰性求值思想減小計算開銷

初看 MobX 源碼,它每每給你一種 」雜項叢生「的感受(調試這段代碼的時候真是內心苦啊),但其實在這背後運轉着一套清晰的 非穩態傳遞非穩態削減 的固定模式,一旦掌握這套模式以後,MobX 自動化響應體系的脈絡已清晰可見,這將爲你更好理解 MobX 的運行機制打下紮實的基礎。

到本篇爲止,咱們已經耗費 3 篇文章來解釋 MobX 的(絕大部分)自動化響應機制。通過這 3 篇文章,讀者應該對 MobX 的整個運起色制有了一個比較清晰明瞭的理解。後續的文章中將逐漸縮減」故事「成分,將講解重心轉移到 MobX 自己概念(好比 ObservabledecoratorAtom等)源碼的解讀上,相信有了這三篇文章的做爲打底,理解其他部分更多的是在語法層面,閱讀起來將更加遊刃有餘。

下面的是個人公衆號二維碼圖片,歡迎關注,及時獲取最新技術文章。
微信公衆號

相關文章
相關標籤/搜索