【用故事解讀 MobX源碼(二)】 computed

================前言===================javascript

=======================================java

在寫本文的時候,因爲 MobX 以及升級到 4.x,API 有較大的變化,所以後續的文章默認都將基於 4.x 以上版本進行源碼閱讀。react

前一篇文章仍然以 mobx v3.5.1 的源碼,autorun 邏輯在新版中沒有更改,所以源碼邏輯仍舊一致。git

A. Story Time

一、 場景

爲了多維度掌控嫌疑犯的犯罪特徵數據,你(警署最高長官)想要獲取並實時監控張三的 貸款數額、存貸比(存款和貸款二者比率) 的變化。github

因而你就擬定了新的命令給執行官 MobX:segmentfault

var bankUser = mobx.observable({
  income: 3,
  debit: 2
});

var divisor = mobx.computed(() => {
  return bankUser.income / bankUser.debit;
});

mobx.autorun(() => {
  console.log('張三的貸款:', bankUser.debit, ';張三的存貸比: ' + divisor);
});

相比上一次的命令,除了監控張三貸款這項直接的指標,還須要監控 貸款比divisor) 這項間接指標。性能優化

執行官 MobX 稍做思忖,要完成這個任務比以前的要難一點點,須要費一點兒精力。微信

clipboard.png

不過,這也難不倒能力強大的 MobX 執行官,一番策略調整以後,從新拿出新的執行方案。部署實施以後,當張三去銀行存款、貸款後,這些變化都實時反饋出來了:ide

clipboard.png

二、部署方案

此次的部署和前一次相差不大,除了須要讓觀察員 O2(監視 income)參與進來以外,考慮到警署最高長官所需的 存貸比divisor),還得派出另外一類職員 —— 會計師

  • 會計師:此類職員專門負責計算,從事 數據的再加工(此項任務中,就是蒐集數據並計算 存貸比

clipboard.png

會計師是一個頗有意思的角色,要想理解他們,必須得思考他們的數據「從哪兒來?到哪裏去?」 這兩個問題:

  • 從哪兒來:從觀察員那兒獲取,也能夠從其餘會計師那兒獲取;
  • 到哪兒去:所生產的數據,要麼是被探長消費,要麼被其餘會計師所用;(固然,沒有人消費他所生產的數據也是可能的,不過這就得追究 MobX 執行官的責任了,浪費了人力資源)

引入了會計師角色以後,MobX 執行官從新繪製了部署計劃圖:

clipboard.png

解釋一下此計劃圖的意思:

  1. 明確這次任務是 當張三帳戶存款或者貸款變動時,打印其貸款數額(debit)和存貸比(divisor
() => {
  console.log('張三的貸款:', bankUser.debit, ';張三的存貸比: ' + divisor);
}
  1. 將任務指派給執行組中的探長 R1
  2. 派遣 2 名觀察組中的觀察員 O一、O2 分別監察張三帳戶的 bankUser.income 屬性和 bankUser.debit 屬性;
  3. 派遣計算組中的會計師 C1 計算張三的貸款比,其所需數值來源於觀察員 O一、O2;
  4. 探長 R1 任務中所需的「張三的帳戶存款」 數值從觀察員 O2 那兒獲取;所需的 「張三的存貸比」 數值從會計師 C1 那兒獲取;
  5. 同時架設數據情報室,方便信息交換;

2.一、部署細節

由於仍是 autorun 命令,因此仍然執行 A計劃方案(詳情參考上一篇《【用故事解讀 MobX源碼(一)】 autorun》)MobX 執行官的部署方案從總體上看是同樣的,考慮到多了會計師這個角色的參與,因此特地在探長 獲取存貸比(divisor 邏輯處空出一部分留給會計師讓它自由發揮:

clipboard.png

這樣作,MobX 執行官也爲了在實際行動中向他的警署長官證明該 A計劃方案 的確擁有「良好的擴展性」。

解開這層新增的會計師計算邏輯 「面紗」,圖示以下:

clipboard.png

你會發現歷史老是驚人的類似,新增的會計師執行計算任務的邏輯其實 探長 執行任務的邏輯是同樣的,下圖中我特地用 相同的序號(不一樣的顏色形狀)標示 出,序號所對應含義以下:

  1. 設置成 正在執勤人員
  2. 開始執行任務
  3. 從觀察員或會計師那兒獲取執行任務所需的數值,並同他們取得聯繫,
  4. 計算任務執行完成後,更新與觀察員 O一、觀察員 O2 之間的聯繫;

clipboard.png

此執行計算任務的邏輯,若是不告訴觀察員的話,觀察員還覺得又來了一名「探長」上級。?

從部署圖裏咱們能夠看出會計師具備兩面性;

  1. 對探長而言:會計師和觀察員地位差很少,都屬於「下級」,都須要將本身的信息及時反饋給探長;
  2. 對觀察員而言:會計師是屬於 「上級」,擁有部分相似探長執行任務權力,只不過其任務類型只能是 計算類型的任務,執行任務結束以後,像探長那樣和觀察員互相關聯起來,方便下一次的運算;

自從有了會計師的參與,探長仍是那個探長,但他的下級已經不是以前的下級了。藉助 A計劃任務的執行,會計師 C1 在上報計算值的時候,會順水推舟地執行計算任務,同時更新他的 」關係網「。

2.二、 懶惰的會計師

會計師有一個特性就是比較懶:就算觀察員所觀察到的值變動了,他們也不會當即從新計算,而只在必要的時候(好比當上級前來索取時)纔會從新計算。

舉個例子,當觀察員 O1 發現張三的帳戶存款從原來的 3 變成 6 :

bankUser.income = 6;

這個時候會觸發一系列的 「漣漪」:

  • ① 觀察員 O1 先註冊事務,至關於到數據情報室」上班打卡「,聲明此次事件由 觀察員 O1 主導
  • ② 告知其上級,也就是會計師 C1 ,說是張三存款(income)有變動
  • ③ 會計師 C1 獲知消息後,」慵懶地「調整本身的狀態
  • ④ 隨後會計師 C1 繼續往上級彙報,告知本會計師的值有更改(注意,此時會計師只是告訴上級本身的值有更改這一事實,但並無執行計算任務 !)
  • ⑤ 探長 R1 接收到會計師的反饋後,就向 MobX 執行官申請要執行任務!由於其下級會計師 C1 彙報說值有更改,說明這個時候應該要從新執行任務啦~
  • ⑥ 執行官 MobX 調閱數據情報室信息一看,發現目前觀察員 O1 正在執行事務,就讓探長 R1 再等等,如今不是執行任務的最佳時機,等到事務結束再說。
  • ⑦ 不一下子觀察員 O1 完成了本身的職責,」下班打卡「,在數據情報室中註銷事務
  • ⑧ 這個時候,執行官 MobX 才讓探長 R1 開始執行任務

將上面的文字轉換成流程圖,能夠清晰看到各角色在此次「漣漪」中所起到的做用:

clipboard.png

這裏須要注意 3 點:

  1. 當觀察員O1 彙報張三存款有更改的時候,會計師 C1 並無當即從新計算值哦,僅僅是更改自身的狀態;
  2. 會計師告知上級(探長 R1)本身有值更改,探長申請執行任務,不過 MobX 執行官並無容許他這麼作,而是讓他先等待一下,由於此時 觀察員 O1 還在彙報工做。等觀察員 O1 工做彙報完畢,這個時候才讓探長執行任務。由於有可能有其餘計算組職員也正在響應該觀察值的更改,事情一件一件來,不要着急,這和 debounce 思想一致,減小沒必要要的計算。
  3. 只有在最後探長執行任務時 須要用到會計師的值的時候,會計師纔會去執行計算操做。這就是典型的惰性求值思惟。

會計師這種拖延到 只有被須要的時候才進行計算 的行爲,有沒有讓你回憶起學生時代寒假結束前一天瘋狂補做業的場景??

clipboard.png

2.三、避免沒必要要的計算

當執行官 MobX 拿着這份執行報告送達給你(警署最高長官),閱覽完畢:」不錯,這套方案的確部分證明了你以前所言的可擴展性。但隨着職員的引入,運起色構逐漸龐大,如何避免沒必要要的開銷的呢?「

」長官您高瞻遠矚,這的確是一個問題。在井井有理的規則下,個別職員的運做效率的確會打折扣。所以避免職員沒必要要的計算開銷,也是在我方案部署規劃以內。正如您所見,上述方案中會計師的‘惰性’、探員在事務以後再進行任務等機制,都是基於優化性能所採起的措施。「 執行官 MobX 稍做停頓,繼續道,」爲了更好地闡述這套運行方案的性能優化機制,我明天呈上一份報告,好讓您得以全面瞭解。「

」Good Job!期待你的報告「。

那麼,執行官 MobX 是憑藉什麼機制減小開銷的呢?且聽下回分解。
(本節完,未完待續)

B. Source Code Time

本節部分,仍然是就着上面的」故事「來說 MobX 中的源碼。

先羅列本文故事中新出現的 會計師 角色與 MobX 源碼概念映射關係:

故事人物 MobX 源碼 解釋
會計師 computedvalue 官方文檔 - (@)computed 計算值
探長、執行官等角色的映射關係,參考上一篇《 【用故事解讀 MobX源碼(一)】 autorun

clipboard.png

本文的重點內容就是 computedvalue 的部分源碼(它在 autorun 等場景中的應用)

autorun(A 計劃)的源碼在上一節講過,這裏再也不贅述。咱們僅僅講解一下 computedValueautorun 中的表現。

一、會計師,請開始你的表演

在故事中咱們講到過,當探長向會計師索要計算值的時候,此時懶惰的會計師爲了 」應付交差「,這時候纔開始計算,其計算的過程和探長執行的任務流程幾乎一致。

從源碼角度去看一下其中的緣由。

當探長執行任務:

() => {
  console.log('張三的貸款:', bankUser.debit, ';張三的存貸比: ' + divisor);
}

任務中也涉及 bankUser.debit 變量和 divisor 變量;其中在獲取 bankUser.debit 變量之時會讓觀察員 O2 觸發 reportObserved方法,這個上一篇文章着重講過,此處就不詳細展開了;而請求 divisor 數值的時候,則會觸發該值的 valueOf() 方法 —— 即調用會計師(computedValue)的 valueOf() 方法。

爲何調用就觸發 valueOf() 方法呢?請看下方的「知識點」備註?

======== 插播知識點 =========

任何原始值仍是對象其實都包含 valueOf()toString() 方法,valueOf() 會返回最適合該對象類型的原始值,toString() 將該對象的原始值以字符串形式返回。
這兩個方法通常是交由 JS 去隱式調用,以知足不一樣的運算狀況。好比在數值運算(如a + b)裏會優先調用 valueOf(),而在字符串運算(如alert(c))裏,會優先調用 toString() 方法
順帶附上兩篇 參考文章

======== 完畢 ==========

一旦調用調用會計師的 valueOf 方法:

valueOf(): T {
    return toPrimitive(this.get())
}

其實就是調用 this.get() 方法,咱們瞧一眼源碼;

clipboard.png

1.一、 重量級計算 仍是 輕量級 計算?

這裏有個分叉點,根據 globalState.inBatch 決定究竟是啓用 重量級計算 仍是 輕量級計算

  • globalState.inBatch 值大於 0,說明會計師被上級徵調(處於上級事務中),好比此案例中,陷於 A 計劃(autorun )的會計師,在上級探長 R1 須要查閱計算值時候,就會進入重量級計算模式
  • 當會計師無上級徵調的時候,globalState.inBatch 值爲 0,就會進入輕量級計算模式,簡化計算的邏輯。

但不管輕量級仍是重量級計算,都會涉及到調用 computeValue() 方法來執行計算任務。

調用的時候,若是是 重量級計算track 這個 bool 值爲 true,不然track 值爲 false

clipboard.png

計算值有個屬性,this.derivation 就是會計師要計算數值時所依據的計算表達式,也就是而咱們定義會計師時所傳入的匿名函數:

() => {
  return bankUser.income / bankUser.debit;
}

不管是 重量級計算 模式仍是 輕量級計算 模式,最終都是會調用該計算表達式獲取計算值

重量級計算 模式和 輕量級計算 模式二者的差異只是在於前者在執行該計算表達式以前會設置不少環境,後者直接就按這個表達式計算數值返回。

在上述的故事中,因爲探長 R1 人物的存在,會計師會執行 重量級計算 模式,接下來的源碼分析也走這條分支路線。( 輕量級計算 模式的狀況當作課後思考題)。

1.二、像探長學習

重量級計算的時候,computeValue(true) 就會走和 探長 操做模式同樣 trackDerivedFunction 步驟。沒錯,探長和會計師調用的就是同一個方法,因此他們在執行任務的時候,行爲痕跡是同樣的,沒毛病。

clipboard.png

若是忘記 trackDerivedFunction 方法內容,請查看 《【用故事解讀 MobX源碼(一)】 autorun》的 」2.2.二、trackDerivedFunction「 部分

只不過會計師只能執行計算類的任務(純函數)罷了,探長能夠執行任意類型的任務。

和探長同樣,會計師執行計算任務完畢以後調用 bindDependencies 將綁定 觀察員 O1 和 觀察員 O2 ;而在執行計算以後,會計師會調用 propagateChangeConfirmed 方法,更改本身和上級 探長 的狀態 —— 這說明,對探長而言,會計師就至關於 觀察員的角色,在探長執行任務結束後像觀察員同樣須要上報本身的計算值,並和 探長 取得聯繫;

這麼看會計師還真 」牆頭草,兩邊倒」。

至此,會計師這個角色以較低的成本就能完美地整合進執行官 MobX 所部署的 A 集合部署方案中。??

二、 響應觀察值的變化

一旦張三的帳戶存款(income)發生變化,將會觸發 MobX 所提供的 reportChanged 方法:

public reportChanged() {
      startBatch()
      propagateChanged(this)
      endBatch()
  }
注意這裏的 startBatchendBatch 方法,說明觀察員 O1 發起事務了。

2.一、傳遞變化的信息

咱們知道(不知道的請閱讀上一篇文章)該 reportChanged() 方法中的 propagateChanged() 會觸發上級的 onBecomeStale() 方法。

觀察員 O1 此時的上級是 會計師 C1,其所定義的 onBecomeStale 以下:

onBecomeStale() {
    propagateMaybeChanged(this)
}

看一下 propagateMaybeChanged(this) 源碼,也比較簡單,主要作了兩件事情,① 會計師會調整自身的狀態; ②而後觸發其上級(探長 R1)的 onBecomeStale() 方法。

clipboard.png

可見觀察員 01 會引發會計師 C1 的響應,而會計師會引發探長 R1 的響應,這種響應「漣漪」就是經過下級觸發上級的 onBecomeStale 方法造成的連鎖反應。

不一樣上級(好比會計師和探長)的 onBecomeStale 定義不一樣。

探長的這個 onBecomeStale 方法在上一篇文章的 「三、響應觀察值的變化 - propagateChanged」 中咱們講過,探長將請求 MobX 請求從新執行一遍 A 計劃方案。

然而,MobX 拒絕了此次請求,讓他再等待一下。??

這是由於在 runReactions 方法中:

if (globalState.inBatch > 0 || globalState.isRunningReactions) return

因爲此時 inBatch 是 1(由於觀察員執行了 startBatch()),因此會直接 return 掉。

直到觀察員執行 endBatch() 的時候,除了會結束本次的上報事務,同時執行官 MobX 會從新執行 runReactions 方法,讓久等的探長去執行任務:

endBatch

探長在執行任務的時候,就會打印張三的貸款(debit)、存貸比(divisor)了。

2.二、雖然懶,可是懶得有技巧

綜上,當張三存款(income)變動,就能讓 A 計劃(autorun)自動運行,探長會打印張三的貸款(debit)、存貸比(divisor)。

這裏須要說起一下,關於會計師從新計算的時機,是在探長執行 shouldCompute 的時候,探長髮現會計師值 陳舊 了,就讓會計師從新計算:

clipboard.png

看看這裏,對計算值而言,isComputedValue()(若是是計算值)返回 true,就會執行 obj.get() 方法,這個方法剛纔剛講過,會讓會計師執行 重量型計算操做,更新本身的計算值。

因此,此次計算時機並不是等到探長執行任務時(真正用到該值)的時候才讓其從新計算,和第一次 autorun 的時機不一致

估計這是 MobX 考慮到會計師的值確定須要更新的(已經肯定要被探長 R1 用到),還有可能會被其餘上級引用,既然早晚要更新的,那就儘量將更新前置,這樣在總體上能下降成本。

更新完以後,在探長執行任務的時候,會計師彙報本身是最新的值了,就不用再從新計算一遍。

雖然懶,可是懶得有技巧。

至此,有關會計師的源碼解讀已經差很少,後續有想到的再補充。

三、其餘說明

本文爲了方便說明,因此單獨使用 mobx.computed 方法定義計算值,平時使用中更多則是直接應用在 對象中屬性 上,使用 get 語法:

var bankUser = mobx.observable({
  income: 3,
  debit: 2,
  get divisor() {
    return this.income / this.debit;
  }
});

這僅僅是寫法上不同,源碼分析的思路是一致的。

四、小測試

4.一、測試1

問題:當咱們更改張三貸款數額 bankUser.debit = 4; 時,請從源碼角度解答 MobX 的執行流程是如何的?

參考答案提示

reportChanged() 
    => propagateChanged() 
    => propagateMaybeChanged() 
    => runReaction() 
    => track() 
    => get() 
    => computeValue() 
    => bindDependencies()

4.二、測試2

問題:若是不存在 autorun (即沒有探長參與,僅有觀察員和會計師),此時僅改變張三存款數值:

var bankUser = mobx.observable({
  income: 3,
  debit: 2
});

var divisor = mobx.computed(() => {
  return bankUser.income / bankUser.debit;
});

bankUser.income = 6; // 請問此時的執行狀況是什麼樣的?

console.log('張三的存貸比:', divisor)

請問會計師會從新計算數值麼?此時這套系統的執行狀況又會是怎麼樣的呢?

參考答案提示:會計師此時執行 輕量級計算模式

五、小結

此篇文章講解 MobX 中 計算值 (computedValue) 的概念,類比故事中的會計師角色。總結一下 計算值 (computedValue)的特徵:

  1. 計算值是基於現有狀態或其餘計算值衍生出的數值,通常是經過 純函數 的方式衍生而得。
  2. 一旦觀察值更改以後,計算值是可以從新執行計算,不過並不是當即執行,而是 惰性 的 ———— 只有在必要的時候纔會執行計算。
  3. 對觀察值而言,計算值和 autorun(或reaction) 很像,之因此類似是在 執行任務 時都涉及到調用 trackDerivedFunction 方法;而對 autorun(或reaction)而言,計算值和觀察值很相,都是數據提供者。

正如 官方文檔 而言,計算值是高度優化過的,因此儘量應用他們。

clipboard.png

下一篇文章將探討 MobX 中與 autoruncomputed 相關的計算性能優化的機制,看看 MobX 如何平衡複雜場景下狀態管理時的效率和性能。

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

相關文章
相關標籤/搜索