摘要:場景為了多維度掌控嫌疑犯的犯罪特征數據,你警署最高長官想要獲取并實時監控張三的貸款數額存貸比存款和貸款兩者比率的變化。
================前言===================
初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式講解源碼;
本系列文章:
《【用故事解讀 MobX源碼(一)】 autorun》
《【用故事解讀 MobX源碼(二)】 computed》
《【用故事解讀 MobX源碼(三)】 shouldCompute》
《【用故事解讀 MobX 源碼(四)】裝飾器 和 Enhancer》
《【用故事解讀 MobX 源碼(五)】 Observable》
文章編排:每篇文章分成兩大段,第一大段以簡單的偵探系列故事的形式講解(所涉及人物、場景都以 MobX 中的概念為原型創建),第二大段則是相對于的源碼講解。
本文基于 MobX 4 源碼講解
=======================================
在寫本文的時候,由于 MobX 以及升級到 4.x,API 有較大的變化,因此后續的文章默認都將基于 4.x 以上版本進行源碼閱讀。
前一篇文章仍然以 mobx v3.5.1 的源碼,autorun 邏輯在新版中沒有更改,因此源碼邏輯仍舊一致。
A. Story Time 1、 場景為了多維度掌控嫌疑犯的犯罪特征數據,你(警署最高長官)想要獲取并實時監控張三的 貸款數額、存貸比(存款和貸款兩者比率) 的變化。
于是你就擬定了新的命令給執行官 MobX:
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 稍作思忖,要完成這個任務比之前的要難一點點,需要費一點兒精力。
不過,這也難不倒能力強大的 MobX 執行官,一番策略調整之后,重新拿出新的執行方案。部署實施之后,當張三去銀行存款、貸款后,這些變化都實時反饋出來了:
2、部署方案這次的部署和前一次相差不大,除了需要讓觀察員 O2(監視 income)參與進來之外,考慮到警署最高長官所需的 存貸比 (divisor),還得派出另一類職員 —— 會計師:
會計師:此類職員專門負責計算,從事 數據的再加工(此項任務中,就是搜集數據并計算 存貸比)
會計師是一個很有意思的角色,要想理解他們,必須得思考他們的數據“從哪兒來?到哪里去?” 這兩個問題:
從哪兒來:從觀察員那兒獲取,也可以從其他會計師那兒獲取;
到哪兒去:所生產的數據,要么是被探長消費,要么被其他會計師所用;(當然,沒有人消費他所生產的數據也是可能的,不過這就得追究 MobX 執行官的責任了,浪費了人力資源)
引入了會計師角色之后,MobX 執行官重新繪制了部署計劃圖:
解釋一下此計劃圖的意思:
明確此次任務是 當張三賬戶存款或者貸款變更時,打印其貸款數額(debit)和存貸比(divisor):
() => { console.log("張三的貸款:", bankUser.debit, ";張三的存貸比: " + divisor); }
將任務指派給執行組中的探長 R1
派遣 2 名觀察組中的觀察員 O1、O2 分別監察張三賬戶的 bankUser.income 屬性和 bankUser.debit 屬性;
派遣計算組中的會計師 C1 計算張三的貸款比,其所需數值來源于觀察員 O1、O2;
探長 R1 任務中所需的“張三的賬戶存款” 數值從觀察員 O2 那兒獲?。凰璧?“張三的存貸比” 數值從會計師 C1 那兒獲取;
同時架設數據情報室,方便信息交換;
2.1、部署細節因為還是 autorun 命令,所以仍然執行 A計劃方案(詳情參考上一篇《【用故事解讀 MobX源碼(一)】 autorun》)MobX 執行官的部署方案從整體上看是一樣的,考慮到多了會計師這個角色的參與,所以特意在探長 獲取存貸比(divisor) 邏輯處空出一部分留給會計師讓它自由發揮:
這樣做,MobX 執行官也為了在實際行動中向他的警署長官證實該 A計劃方案 的確擁有“良好的擴展性”。
解開這層新增的會計師計算邏輯 “面紗”,圖示如下:
你會發現歷史總是驚人的相似,新增的會計師執行計算任務的邏輯其實 探長 執行任務的邏輯是一樣的,下圖中我特意用 相同的序號(不同的顏色形狀)標示 出,序號所對應含義如下:
設置成 正在執勤人員
開始執行任務
從觀察員或會計師那兒獲取執行任務所需的數值,并同他們取得聯系,
計算任務執行完成后,更新與觀察員 O1、觀察員 O2 之間的聯系;
此執行計算任務的邏輯,如果不告訴觀察員的話,觀察員還以為又來了一名“探長”上級。?
從部署圖里我們可以看出會計師具有兩面性;
對探長而言:會計師和觀察員地位差不多,都屬于“下級”,都需要將自己的信息及時反饋給探長;
對觀察員而言:會計師是屬于 “上級”,擁有部分類似探長執行任務權力,只不過其任務類型只能是 計算類型的任務,執行任務結束之后,像探長那樣和觀察員互相關聯起來,方便下一次的運算;
自從有了會計師的參與,探長還是那個探長,但他的下級已經不是之前的下級了。借助 A計劃任務的執行,會計師 C1 在上報計算值的時候,會順水推舟地執行計算任務,同時更新他的 ”關系網“。
2.2、 懶惰的會計師會計師有一個特性就是比較懶:就算觀察員所觀察到的值變更了,他們也不會立即重新計算,而只在必要的時候(比如當上級前來索取時)才會重新計算。
舉個例子,當觀察員 O1 發現張三的賬戶存款從原來的 3 變成 6 :
bankUser.income = 6;
這個時候會觸發一系列的 “漣漪”:
① 觀察員 O1 先注冊事務,相當于到數據情報室”上班打卡“,聲明這次事件由 觀察員 O1 主導
② 告知其上級,也就是會計師 C1 ,說是張三存款(income)有變更
③ 會計師 C1 獲知消息后,”慵懶地“調整自己的狀態
④ 隨后會計師 C1 繼續往上級匯報,告知本會計師的值有更改(注意,此時會計師只是告訴上級自己的值有更改這一事實,但并沒有執行計算任務 ?。?/p>
⑤ 探長 R1 接收到會計師的反饋后,就向 MobX 執行官申請要執行任務!因為其下級會計師 C1 匯報說值有更改,說明這個時候應該要重新執行任務啦~
⑥ 執行官 MobX 調閱數據情報室信息一看,發現目前觀察員 O1 正在執行事務,就讓探長 R1 再等等,現在不是執行任務的最佳時機,等到事務結束再說。
⑦ 不一會兒觀察員 O1 完成了自己的職責,”下班打卡“,在數據情報室中注銷事務
⑧ 這個時候,執行官 MobX 才讓探長 R1 開始執行任務
將上面的文字轉換成流程圖,可以清晰看到各角色在這次“漣漪”中所起到的作用:
這里需要注意 3 點:
當觀察員O1 匯報張三存款有更改的時候,會計師 C1 并沒有立即重新計算值哦,僅僅是更改自身的狀態;
會計師告知上級(探長 R1)自己有值更改,探長申請執行任務,不過 MobX 執行官并沒有允許他這么做,而是讓他先等待一下,因為此時 觀察員 O1 還在匯報工作。等觀察員 O1 工作匯報完畢,這個時候才讓探長執行任務。因為有可能有其他計算組職員也正在響應該觀察值的更改,事情一件一件來,不要著急,這和 debounce 思想一致,減少不必要的計算。
只有在最后探長執行任務時 需要用到會計師的值的時候,會計師才會去執行計算操作。這就是典型的惰性求值思維。
會計師這種拖延到 只有被需要的時候才進行計算 的行為,有沒有讓你回憶起學生時代寒假結束前一天瘋狂補作業的場景??
2.3、避免不必要的計算當執行官 MobX 拿著這份執行報告送達給你(警署最高長官),閱覽完畢:”不錯,這套方案的確部分證實了你之前所言的可擴展性。但隨著職員的引入,運轉機構逐漸龐大,如何避免不必要的開銷的呢?“
”長官您高瞻遠矚,這的確是一個問題。在井然有序的規則下,個別職員的運作效率的確會打折扣。因此避免職員不必要的計算開銷,也是在我方案部署規劃之內。正如您所見,上述方案中會計師的‘惰性’、探員在事務之后再進行任務等機制,都是基于優化性能所采取的措施。“ 執行官 MobX 稍作停頓,繼續道,”為了更好地闡述這套運行方案的性能優化機制,我明天呈上一份報告,好讓您得以全面了解?!?/p>
”Good Job!期待你的報告“。
那么,執行官 MobX 是憑借什么機制減少開銷的呢?且聽下回分解。
(本節完,未完待續)
本節部分,仍然是就著上面的”故事“來講 MobX 中的源碼。
先羅列本文故事中新出現的 會計師 角色與 MobX 源碼概念映射關系:
故事人物 | MobX 源碼 | 解釋 |
---|---|---|
會計師 | computedvalue | 官方文檔 - (@)computed 計算值 |
探長、執行官等角色的映射關系,參考上一篇《【用故事解讀 MobX源碼(一)】 autorun》
本文的重點內容就是 computedvalue 的部分源碼(它在 autorun 等場景中的應用)
autorun(A 計劃)的源碼在上一節講過,這里不再贅述。我們僅僅講解一下 computedValue 在 autorun 中的表現。
1、會計師,請開始你的表演在故事中我們講到過,當探長向會計師索要計算值的時候,此時懶惰的會計師為了 ”應付交差“,這時候才開始計算,其計算的過程和探長執行的任務流程幾乎一致。
從源碼角度去看一下其中的原因。
當探長執行任務:
() => { 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() 方法
順帶附上兩篇 參考文章
js中 toString 和 valueOf 的區別?:知乎問答
valueOf() vs. toString() in Javascript:SF 上的回答,非常詳盡地告訴你其執行結果
======== 完畢 ==========
一旦調用調用會計師的 valueOf 方法:
valueOf(): T { return toPrimitive(this.get()) }
其實就是調用 this.get() 方法,我們瞧一眼源碼;
1.1、 重量級計算 還是 輕量級 計算?這里有個分叉點,根據 globalState.inBatch 決定到底是啟用 重量級計算 還是 輕量級計算:
當 globalState.inBatch 值大于 0,說明會計師被上級征調(處于上級事務中),比如此案例中,陷于 A 計劃(autorun )的會計師,在上級探長 R1 需要查閱計算值時候,就會進入重量級計算模式
當會計師無上級征調的時候,globalState.inBatch 值為 0,就會進入輕量級計算模式,簡化計算的邏輯。
但無論輕量級還是重量級計算,都會涉及到調用 computeValue() 方法來執行計算任務。
調用的時候,如果是 重量級計算 則 track 這個 bool 值為 true,否則track 值為 false。
計算值有個屬性,this.derivation 就是會計師要計算數值時所依據的計算表達式,也就是而我們定義會計師時所傳入的匿名函數:
() => { return bankUser.income / bankUser.debit; }
無論是 重量級計算 模式還是 輕量級計算 模式,最終都是會調用該計算表達式獲取計算值。
重量級計算 模式和 輕量級計算 模式兩者的差別只是在于前者在執行該計算表達式之前會設置很多環境,后者直接就按這個表達式計算數值返回。
在上述的故事中,由于探長 R1 人物的存在,會計師會執行 重量級計算 模式,接下來的源碼分析也走這條分支路線。( 輕量級計算 模式的情況當做課后思考題)。
1.2、像探長學習在 重量級計算的時候,computeValue(true) 就會走和 探長 操作模式一樣 trackDerivedFunction 步驟。沒錯,探長和會計師調用的就是同一個方法,所以他們在執行任務的時候,行為痕跡是一樣的,沒毛病。
如果忘記 trackDerivedFunction 方法內容,請查看 《【用故事解讀 MobX源碼(一)】 autorun》的 ”2.2.2、trackDerivedFunction“ 部分
只不過會計師只能執行計算類的任務(純函數)罷了,探長可以執行任意類型的任務。
和探長一樣,會計師執行計算任務完畢之后調用 bindDependencies 將綁定 觀察員 O1 和 觀察員 O2 ;而在執行計算之后,會計師會調用 propagateChangeConfirmed 方法,更改自己和上級 探長 的狀態 —— 這說明,對探長而言,會計師就相當于 觀察員的角色,在探長執行任務結束后像觀察員一樣需要上報自己的計算值,并和 探長 取得聯系;
這么看會計師還真 ”墻頭草,兩邊倒”。
至此,會計師這個角色以較低的成本就能完美地整合進執行官 MobX 所部署的 A 集合部署方案中。??
2、 響應觀察值的變化一旦張三的賬戶存款(income)發生變化,將會觸發 MobX 所提供的 reportChanged 方法:
public reportChanged() { startBatch() propagateChanged(this) endBatch() }
注意這里的 startBatch 和 endBatch 方法,說明觀察員 O1 發起事務了。2.1、傳遞變化的信息
我們知道(不知道的請閱讀上一篇文章)該 reportChanged() 方法中的 propagateChanged() 會觸發上級的 onBecomeStale() 方法。
觀察員 O1 此時的上級是 會計師 C1,其所定義的 onBecomeStale 如下:
onBecomeStale() { propagateMaybeChanged(this) }
看一下 propagateMaybeChanged(this) 源碼,也比較簡單,主要做了兩件事情,① 會計師會調整自身的狀態; ②然后觸發其上級(探長 R1)的 onBecomeStale() 方法。
可見觀察員 01 會引起會計師 C1 的響應,而會計師會引起探長 R1 的響應,這種響應“漣漪”就是通過下級觸發上級的 onBecomeStale 方法形成的連鎖反應。
不同上級(比如會計師和探長)的 onBecomeStale 定義不同。
探長的這個 onBecomeStale 方法在上一篇文章的 “3、響應觀察值的變化 - propagateChanged” 中我們講過,探長將請求 MobX 請求重新執行一遍 A 計劃方案。
然而,MobX 拒絕了這次請求,讓他再等待一下。??
這是因為在 runReactions 方法中:
if (globalState.inBatch > 0 || globalState.isRunningReactions) return
由于此時 inBatch 是 1(因為觀察員執行了 startBatch()),所以會直接 return 掉。
直到觀察員執行 endBatch() 的時候,除了會結束本次的上報事務,同時執行官 MobX 會重新執行 runReactions 方法,讓久等的探長去執行任務:
探長在執行任務的時候,就會打印張三的貸款(debit)、存貸比(divisor)了。
2.2、雖然懶,但是懶得有技巧綜上,當張三存款(income)變更,就能讓 A 計劃(autorun)自動運行,探長會打印張三的貸款(debit)、存貸比(divisor)。
這里需要提及一下,關于會計師重新計算的時機,是在探長執行 shouldCompute 的時候,探長發現會計師值 陳舊 了,就讓會計師重新計算:
看看這里,對計算值而言,isComputedValue()(如果是計算值)返回 true,就會執行 obj.get() 方法,這個方法剛才剛講過,會讓會計師執行 重量型計算操作,更新自己的計算值。
所以,這次計算時機并非等到探長執行任務時(真正用到該值)的時候才讓其重新計算,和第一次 autorun 的時機不一致。
估計這是 MobX 考慮到會計師的值肯定需要更新的(已經確定要被探長 R1 用到),還有可能會被其他上級引用,既然遲早要更新的,那就盡可能將更新前置,這樣在整體上能降低成本。
更新完之后,在探長執行任務的時候,會計師匯報自己是最新的值了,就不用再重新計算一遍。
雖然懶,但是懶得有技巧。
至此,有關會計師的源碼解讀已經差不多,后續有想到的再補充。
3、其他說明本文為了方便說明,所以多帶帶使用 mobx.computed 方法定義計算值,平時使用中更多則是直接應用在 對象中屬性 上,使用 get 語法:
var bankUser = mobx.observable({ income: 3, debit: 2, get divisor() { return this.income / this.debit; } });
這僅僅是寫法上不一樣,源碼分析的思路是一致的。
4、小測試 4.1、測試1問題:當我們更改張三貸款數額 bankUser.debit = 4; 時,請從源碼角度解答 MobX 的執行流程是如何的?
參考答案提示:
reportChanged() => propagateChanged() => propagateMaybeChanged() => runReaction() => track() => get() => computeValue() => bindDependencies()4.2、測試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)
請問會計師會重新計算數值么?此時這套系統的執行情況又會是怎么樣的呢?
參考答案提示:會計師此時執行 輕量級計算模式。
5、小結此篇文章講解 MobX 中 計算值 (computedValue) 的概念,類比故事中的會計師角色。總結一下 計算值 (computedValue)的特征:
計算值是基于現有狀態或其他計算值衍生出的數值,一般是通過 純函數 的方式衍生而得。
一旦觀察值更改之后,計算值是能夠重新執行計算,不過并非立即執行,而是 惰性 的 ———— 只有在必要的時候才會執行計算。
對觀察值而言,計算值和 autorun(或reaction) 很像,之所以相似是在 執行任務 時都涉及到調用 trackDerivedFunction 方法;而對 autorun(或reaction)而言,計算值和觀察值很相,都是數據提供者。
正如 官方文檔 而言,計算值是高度優化過的,所以盡可能應用他們。
下一篇文章將探討 MobX 中與 autorun 和 computed 相關的計算性能優化的機制,看看 MobX 如何平衡復雜場景下狀態管理時的效率和性能。
下面的是我的公眾號二維碼圖片,歡迎關注,及時獲取最新技術文章。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/94056.html
摘要:所以這是一篇插隊的文章,用于去理解中的裝飾器和概念。因此,該的作用就是根據入參返回具體的描述符。其次局部來看,裝飾器具體應用表達式是,其函數簽名和是一模一樣。等裝飾器語法,是和直接使用是等效等價的。 ================前言=================== 初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式講解源碼; 本系列文章: 《【用故事解...
摘要:最簡單的情況張三的存貸這里我們創建了實例探長實例觀察員這個示例和我們之前在首篇文章用故事解讀源碼一中所用示例是一致的。 ================前言=================== 初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式講解源碼; 本系列文章: 《【用故事解讀 MobX源碼(一)】 autorun》 《【用故事解讀 MobX源碼(二)】...
摘要:隨后,執行官給出一張當張三存款發生變化之時,此機構的運作時序圖的確,小機構靠人力運作,大機構才靠制度運轉。第一條語句創建觀察員第一條語句張三我們調用的時候,就創建了對象,對象的所有屬性都將被拷貝至一個克隆對象并將克隆對象轉變成可觀察的。 ================前言=================== 初衷:網上已有很多關于 MobX 源碼解讀的文章,但大多閱讀成本甚高。...
摘要:前言初衷以系列故事的方式展現源碼邏輯,盡可能以易懂的方式講解源碼本系列文章用故事解讀源碼一用故事解讀源碼二用故事解讀源碼三用故事解讀源碼四裝飾器和用故事解讀源碼五文章編排每篇文章分成兩大段,第一大段以簡單的偵探系列故事的形式講解所涉及人物場 ================前言=================== 初衷:以系列故事的方式展現 MobX 源碼邏輯,盡可能以易懂的方式...
閱讀 2248·2021-11-22 09:34
閱讀 2012·2021-09-22 15:22
閱讀 2015·2019-08-29 15:05
閱讀 2105·2019-08-26 10:43
閱讀 3406·2019-08-26 10:26
閱讀 876·2019-08-23 18:29
閱讀 3518·2019-08-23 16:42
閱讀 1994·2019-08-23 14:46