用故事解讀 MobX 源碼(一)autorun

  • 2019 年 10 月 10 日
  • 筆記

溫馨提示:因微信中外鏈都無法點擊,請通過文末的」 「閱讀原文」 到技術博客中完整查閱版;(本文整理自技術博客)

  • 初衷:網上已有很多關於 MobX 源碼解讀的文章,但大多閱讀成本甚高。本人在找文章時對此深有體會,故將以系列故事的方式展現源碼邏輯,儘可能以易懂的方式講解 MobX 源碼;
  • 本系列文章
  • 《【用故事解讀 MobX源碼(一)】 autorun》
  • 《【用故事解讀 MobX源碼(二)】 computed》
  • 《【用故事解讀 MobX源碼(三)】 shouldCompute》
  • 《【用故事解讀 MobX 源碼(四)】裝飾器 和 Enhancer》
  • 《【用故事解讀 MobX 源碼(五)】 Observable》
  • 文章編排:每篇文章分成兩大段,第一大段以簡單的偵探系列故事的形式講解(所涉及人物、場景都以 MobX 中的概念為原型創建),第二大段則是相對於的源碼講解。
  • 本文基於 MobX 3 源碼講解

A. Story Time

1、 場景


場景: 一位名為 張三 的銀行用戶賬戶情況為:

  • 賬戶存款為 3(萬元)
  • 信用借貸為 2(萬元)

你作為警署最高長官,在一起金融犯罪中認定 張三 為金融犯罪嫌疑犯,想自動化跟蹤這位用戶的儲蓄情況,比如他一旦銀行存款有變更就打印出他當前的賬戶存款


為了實現這個任務,你下發命令給你的執行官(MobX):

var bankUser = mobx.observable({  name: '張三',  income: 3,  debit: 2  });    mobx.autorun(() => {  console.log('張三的賬戶存款:', bankUser.income);  });  

部署任務

執行官拿着這幾行代碼開始部署警力來完成你下發的指令,並將這次行動命名為 A計劃 (是不是很酷???)。你所要做的,就是等執行官 MobX 執行行動部署完畢之後,坐在辦公室里一邊愜意地喝着咖啡,一邊在電腦上觀察張三賬戶的存款變化。

執行官部署完畢後,首先會立即打印出 張三的賬戶存款: 3

張三的存款

後續張三的賬戶存款有更改的話,會 自動執行該部署方案,控制台里就自動打印其存款;

// 更改賬戶存款  bankUser.income = 4;  bankUser.income = 10;  

張三的存款正在變化

autorun

是不是很神奇很自動化?

2、 部署方案

作為警署最高長官,你不必事必躬親過問執行官(MobX)部署的細節,只要等着要結果就可以。

而作為執行官(MobX),你得知道 A計劃 中部署方案的每一步細節。下面我們來一探究竟執行官 MobX 到底是如何部署 A計劃 的。

2.1、 組織架構

執行官(MobX) 擁有一套成熟的運作機構組織支撐任務的執行。為了執行這項任務,涉及到 2 類職員和 1 個數據情報室:

  • 觀察員:其工作職責是觀察並監督嫌疑人特定信息,比如這裡,監視張三的收入(income)屬性,當這項特徵有變更的時候,及時向上級彙報(並執行特定的操作);

觀察員

  • 探長:一方面負責管理劃歸給他的 觀察員,整合觀察員反饋的資訊;另一方面接受 MobX 執行官交給他的任務,在 適當的時機 執行這項任務(此任務是打印張三的存款);

探長

  • 此外還會架設一個 數據情報室,方便執行官 MobX、探長和觀察員們 互相通過情報室進行數據信息的交換。

數據情報室

具體組織架構關係圖如下:

組織架構關係圖

按照組織架構,執行官 MobX 分解計劃細節並安排人員如下:

1.明確此次任務是 當張三賬戶存款變更時,打印其存款

() => {  console.log('張三的賬戶存款:', bankUser.income);  }  

2.將任務指派給執行組中的探長 R1 3.派遣觀察組中的觀察員 O1 監察張三賬戶的 bankUser.income 屬性 4.探長 R1 任務中所需的「張三的賬戶存款」 數值必須從觀察員 O1 那兒獲取; 5.同時架設數據情報室,方便信息交換;

2.2、 部署細節

人員安排完畢,執行官拿出一份 部署方案書,一聲令下 「各位就按這套方案執行任務吧!」;

在部署方案中下達之後,機構各組成員各司其職,開始有條不紊地開始運作,具體操作時序圖如下所示:

操作時序圖

對時序圖中的關鍵點做一下解釋:

  1. 執行官 MobX 先將探長 R1 信息註冊到中心情報室;(有些情況下,比如偵破大案要案時需要多位探長協作,將存在多位探長同時待命的情況;當然此次任務中,只有探長 R1 在待命)
  2. 中心情報室給執行官 MobX 返回所有待命探長列表(此例中僅有探長 R1);
  3. 執行官 MobX 挨個讓每位待命探長按以下步驟操作:
  • 3.1. 探長出發做任務時,記得給中心情報室通告一聲,好比上班「打卡」操作。(記錄事務序號,好讓中心情報室知曉案情複雜度;有些案件中將有很多探長同時執行任務)
  • 3.2 探長 R1 開始監督並執行 MobX 交給他的任務(「打印張三的存款」)
  • 3.3 首先在數據情報室中「註冊」,將自己設置成 正在執勤人員;(這一步很重要)
  • 3.4 隨後真正進入執行任務的狀態
  • 3.5 在執行任務的時候,發現需要張三的存款(income)這個數值,可這個數值探長 R1 不能直接獲取,必須通過觀察員 O1 取得,於是通過專用通訊機和觀察員 O1 取得聯繫,請求獲取要張三的存款(income
  • 3.6 觀察員 O1 發現有人通過專用通訊機請求張三的存款(income),就開始如下操作:
  • 3.6.1 將自己的信息 經過 數據情報室,然後傳達給請求方;只有上級(不一定是探長,有可能是其他的上級領導)才能通過這個專用通訊機發消息給觀察員;
  • 3.6.2 數據情報室 將該信息同步給 正在執勤人員 —— 即探長 R1
  • 3.6.3 同時將張三的存款(income)返回給請求方;(該消息不用經過 數據情報室
  • 3.7 此時探長擁有兩份信息:任務所需要的張三的存款(income),以及觀察員 O1 的相關信息;因時間緊,執行任務優先級高,探長 R1 先拿着張三的存款(income)數據,先做完任務。(至於觀察員 O1 的信息 先臨時保存 ,方便後續處理);
  • 3.8 等到任務執行完了,探長鬆了一口氣,匯總並整理臨時保存的觀察員信息;在這一階段,探長 R1 才和 觀察員 O1 互相建立牢固的關係(可以理解為,互留聯繫方式,可隨時聯繫得上對方),也是在這個時候,觀察員 O1 才知曉其上級領導是探長 01
  • 3.9 此後,探長 R1 發消息告知中心情報室,削去此次事務(說明事務執行完了),好比下班 「打卡」操作。
  1. 至此 A 計劃部署完畢

上述時序圖中有些地方需要強調一下:

  1. 張三的存款(income)只歸觀察員 O1 監視,探長 R1 所獲取的張三的存款只能通過觀察員 O1 間接獲取到,探長不能越權去直接獲取;
  2. 探長 R1 和觀察員 O1 建立關係並非一步到位,是 分兩個階段 的:
  • 第一階段(對應上述步驟中 3.6.2)是在執行任務期間,僅僅是建立短暫的 單向關係即,此時探長 R1 知曉觀察員 O1 的情況,但反過來,但觀察員 O1 並不知曉探長 R1 ;
  • 第二階段(對應上述步驟中 3.8)在任務執行完,收尾階段的時候,探長才有時間梳理、整合任務期間的探員信息(因為任務中涉及到有可能多個觀察員,當然此次任務中只有 1 個),那時候才有時間 慢慢地 和各位探員互相交換信息,建立 明確且牢固 的關係;

2.3、 任務執行自動化

作為警署最高長官的你,拿着這份部署方案,眉頭緊鎖:「我說執行官 ,就為了區區獲取張三的存款這麼件事兒,耗費那麼多人力資源,值么?直接獲取 bankUser.income 不就行了?!」

「emm…,這所做的努力,圖的是普適性和 自動化響應。」執行官 MobX 淡然自如,不緊不慢徐徐道來,「有了上面那套機制,一方面每當張三的存款變更後,就會 自動化執行上述部署方案的過程;另一方面很方便擴展,後續針對其他監察,只需要在此部署方案中稍加改動就可以,所需要的高級功能都是基於這個方案做延伸。真正做到了 』部署一次,全自動化執行『 的目的。「

隨後,執行官 MobX 給出一張當張三存款發生變化之時,此機構的運作時序圖;

當張三存款發生變化時的運作時序圖

"的確,小機構靠人力運作,大機構才靠制度運轉。那就先試運行這份部署計劃,看它能否經受得起時間的考驗吧。" 警署最高長官拍拍執行官 MobX 的肩膀,若有所思地踱步出了辦公室。

(此節完。未完待續)

B. Source Code Time

上面講那麼久的故事,是為了給講源碼做鋪墊。

接下來將會貼 MobX 源碼相關的代碼,稍顯枯燥,我只能盡量用通俗易懂的話來分析講解。

先羅列本文故事中人物與 MobX 源碼概念映射關係:

本文故事中人物與 MobX 源碼概念映射關係

本文的重點是講解 A 計劃所對應的 autorun 的源碼,先從整體上對 MobX 的運行有個大致了解,而所涉及到的 ReactionObservable 等細節概念後續章節再做展開,這裡僅僅大致提及其部分功能和屬性;

1、下達的命令

回到故事的最開始,你給 MobX 下達的命令如下:

var bankUser = mobx.observable({  name: '張三',  income: 3,  debit: 2  });    mobx.autorun(() => {  console.log('張三的賬戶存款:', bankUser.income);  });  

只有兩條語句,第一條語句是創建觀察員,第二條語句是執行 A 計劃(內含委派探長、架設情報局等工作)

我們挨個細分講解。

1.1、第一條語句:創建觀察員 – Observable

第一條語句:

const bankUser = mobx.observable({  name: '張三',  income: 3,  debit: 2  })  

我們調用 mobx.observable 的時候,就創建了 Observable 對象,對象的所有屬性都將被拷貝至一個克隆對象並將克隆對象轉變成可觀察的。

因此這一行代碼執行後, nameincomedebit 這三個屬性都變成可觀察的;

若以故事場景來敘述中,執行官 MobX 在部署的時候委派了 3 位探員,分別監視這 3 個屬性;而故事中交給探長任務中僅僅涉及了那位監視 income 屬性的觀察員 O1;(所以另外兩位探員都還在休息)

在這裡可以看到 惰性求值 的思想的應用,只有在 必要的時候 啟用 所觀察對象,粒度細,有利於性能提升;

只有 1 位觀察員

之所以只有 1 位觀察員,是因為由於上級下達的具體任務內容是:

() => {  console.log('張三的賬戶存款:', bankUser.income);  }  

看看任務中出現 bankUser.income 而並沒有出現 bankUser.debitbankUser.name,說明這個任務只 牽連 探員O1,而和其他探員無關。

註:本文暫時先不分析 mobx.observable 的源碼,留待後續專門的一章來分析;迫不及待的讀者,可以先閱讀網上其他源碼文章,比如:Mobx 源碼解讀(二) Observable

觀察員有兩個非常重要的行為特徵:

  • 當有人請求觀察員所監控的值(比如income)時,會觸發 MobX 所提供的 reportObserved 方法;
  • 當觀察員所監控的值(比如income)發生變化時,會觸發 MobX 所提供的 propagateChanged 方法;

這裡留一個印象,本文後續在適當的時機再講解這兩個方法是在什麼時候觸發的;

1.2、第二條語句:A 計劃的實施 – autorun

第二條語句:

mobx.autorun(() => {  console.log('張三的賬戶存款:', bankUser.income);  });  

這裡出現的 MobX 中的 mobx.autorun 方法對應故事中的 整個A計劃的實施

A 計劃 對應 autorun 方法

autorun 的直觀含義就是 響應式函數 —— 響應觀察值的變化而自動執行指定的函數。

我們看一下其源碼:

附源碼位置:autorun

autorun 源碼

從這裡可以看出 autorun 大致的脈絡如下:

首先創建 Reaction 類型對象new Reaction 操作可以理解為創建探長 R1 ;

探長對應的類是 Reaction,其關鍵特徵是 監督並控制任務的執行

探長對應的 Reaction 類

本文的下一節將詳細介紹探長們的 "生活日常",此處先放一放。

其次分配任務。源碼中所涉及到的 view() 方法 就是具體的任務內容,即上述故事中的 打印張三賬戶存款 這項任務:

() => {  console.log('張三的賬戶存款:', bankUser.income);  }  

③ 最後,立即執行一次部署方案

代碼中的 reaction.schedule() 表示讓探長 R1 立即執行執行一次部署任務,執行的結果是完成人員部署,並讓探長 R1 打印了一次張三賬戶存款;(同時和觀察員 O1 建立關係)

現在你應該會理解官方文檔中的那句 」使用 autorun 時,所提供的函數總是立即被觸發一次「 話了。

官方文檔對 autorun 的解釋

看一下 schedule 方法:

schedule 方法源碼

看上去很簡單,不到 5 行代碼做了兩件事情: ① 將探長入列; ② 讓隊列中的 所有探長(當然,在我們的示例中僅僅只有 1 名探長)都執行 runReaction 方法

對應時序圖中所標註的 1、2 兩部分:

schedule 方法對應時序圖中所標註的 1、2 兩部分

所謂的 部署(schedule) 就是敦促 各位探長執行 runReaction 方法

第二條語句從整體上看就這樣了。

接下來就讓我們來詳細分析探長的 runReaction 的方法,在該方法中 探長將聯動觀察員、數據情報室一起在部署方案中發揮監督、自動化響應功能

2、每位探長的生活日常

任務的執行全靠探長,不過探長的存在常常是 依賴觀察員 的,這是因為在任務過程中,如果想要獲取所監視的張三的存款(income),必須通過觀察員獲取,自身是沒有權力繞過觀察員直接獲取的哦

每位探長的任務執行流大致如下:

每位探長的任務執行流 主流程大致只有 4 步: ① 開始執行(runReaction) ② 判斷是否執行(shouldCompute) ③ 執行任務(onInvalidate) ④ 結束

這些基就是每位探長的生活的總體了。下面我們挑其中的第 ① 、 ③ 步來講解。

其實圖中另外有一個很重要的 shouldCompute 判斷方法步驟,根據這個方法探長可以自行判斷 是否執行任務,並非所有的任務都需要執行,這一步的作用是優化 MobX 執行效率。該方法源碼內容先略過,後續章節再展開。

2.1、開始執行 – runReaction

runReaction 源碼

該函數比較簡單,主要是為執行任務 」做一些準備「,給任務營造氛圍。用 startBatch() 開頭,用 endBatch() 結尾,中間隔着 onInvalidate

startBatch()endBatch() 這兩個方法一定是成對出現,用於影響 globalStateinBatch 屬性,表明開啟/關閉 一層新的事務,可以理解為 上下班打卡 操作。

只不過 startBatch() 是 」上班打卡「,對應時序圖(3.1) 部分:

startBatch 相當於上班打卡

endBatch() 相當於 「下班打卡」,不過稍微複雜一些,包含一些 收尾 操作,對應時序圖(3.9)部分:

endBatch 相當於下班打卡

我們繼續看隔在中間的 onInvalidate 方法。?

2.2、執行任務 – onInvalidate

此階段是流程中最重要的階段。

你翻看源碼,將會發現此方法 onInvalidate 是 Reaction 類的一個屬性,且在初始化 Reaction 時傳入到構造函數中的,這樣做的目的是方便做擴展。

所以,autorun 方法本質就是一種預定義好的 Reaction —— 你可以依葫蘆畫瓢,將自定義 onInvalidate 方法傳給 Reaction 來實現自己的 計劃任務(什麼 Z計劃啊、阿波羅計劃啊,名字都起好了,就差實現了!!….);

回過頭來,在剛才所述的 autorun 源碼中找到 Reaction 類初始化部分:

const reaction = new Reaction(name, function() {  this.track(reactionRunner)  })  

可以看到 onInvalidate 方法就是:

function() {  this.track(reactionRunner)  }  

這就不難理解 onInvalidate 實際執行的是 reaction.track 方法。

繼續跟蹤源碼,會發現該 onInvalidate 階段主要是由 3 個很重要的子流程所構成:

  • 3.1 跟蹤任務(track)
  • 3.2 執行任務(trackDerivedFunction)
  • 3.3 更新依賴(bindDependencies)

這 3 個函數並非是並行關係,而是嵌套關係,後者是嵌套在前者內執行的:

執行方式很像洋蔥圈模型

題外話:是不是很像 Koa 的 洋蔥圈模型 ??

2.2.1、track

track 源碼

track 方法內容也簡單,和剛才所說的 runReaction 方法類似 —— 也是用 startBatch() 開頭,用 endBatch() 結尾,中間隔着 trackDerivedFunction

所以在這個案例中,整個部署階段是執行 兩次 startBatch()endBatch() 的;在往後複雜的操作中,執行的次數有可能更多。

我們都知道數據庫中的事務概念,其表示一組原子性的操作。Mobx 則借鑒了 事務 這個概念,它實現比較簡單,就是通過 成對 使用 startBatchendBatch 來開始和結束一個事務,用於批量處理 Reaction 的執行,避免不必要的重新計算。

因此到目前這一步,MobX 程序正處在 第二層 事務中。

MobX 中的事務概念

MobX 暴露了 transaction 這一底層 API 供用戶調用,讓用戶能夠實現一些較為高級的應用,具體可參考 官方文檔 – Transaction(事務) 章節獲取更多信息。

接下來繼續看隔在中間的 trackDerivedFunction 方法。

2.2.2、trackDerivedFunction

trackDerivedFunction 源碼

我們總算到了探長 真正執行任務 的步驟了,之前講的所有流程都是為了這個函數服務的。

該環節的第 1 條語句:

globalState.trackingDerivation = derivation;  

對應時序圖(3.3):

探長真正執行任務對應時序圖(3.3)

作用是將 derivation (此處等同於 reaction 對象)掛載到 」全局變量「 globalStatetrackingDerivation 屬性上,這樣其他對象就能獲取到該 derivation 對象的數據了。這好比將探長在數據情報室中註冊為 正在執勤人員,後續觀察員 O1 會向數據情報室索取 正在執勤人員 人,然後將自身信息輸送給他 —— 從結果上看,就相當於 觀察員 O1 直接和 探長 R1 彙報;(之所以要經由數據情報室,是因為在執行任務時候,有可能其他工種的人也需要 正在執勤人員 的信息)

該環節的第 2 條語句:

result = f.call(context); // 效果等同於 result = console.log('張三的賬戶存款:', bankUser.income);  

對應時序圖(3.4):

探長執行對應任務時序圖(3.4)

沒錯,就是本次部署的 終極目的 —— 打印張三賬戶存款!

MobX 將真正的目的執行之前里三層外三層地包裹其他操作,是為了將任務的運行情況控制在自己營造的環境氛圍中。為什麼這麼做呢?

這麼做是基於一個前提,該前提是:所運行的任務 MobX 它無法控制(警署長官今天下達 A 命令,明天下達 B 命令,控制不了)

所以 MobX 就將任務的執行籠罩在自己所營造的氛圍中,改變不了任務實體,我改變環境總行了吧?!!

由於環境是自己營造的,MobX 可以為所欲為,在環境中穿插各種因素:探長、觀察員、數據情報室等等(後續還有其他角色),這樣就將任務的運行盡最大可能地控制在這套所創造的體系中 —— 孫猴子不也翻不出如來佛的五指山么?

雖然更改不了任務內容,不過 MobX 實際在任務中安插觀察員 O1 了,所以呢,當探長在執行任務時,將觸發時序圖中 (3.5)(3.6)兩步反應:

探長在執行任務時,將觸發時序圖中 (3.5)(3.6)兩步反應

複雜么?也還好,(3.6)是由 (3.5)觸發的,(3.5)對應的操作是:探長 R1 想要獲取的張三 income 屬性。 (所以,劃重點,敲黑板!!如果任務中不涉及到 income 這項屬性,那麼就不會有 (3.5)的操作,也就沒有 (3.6)什麼事)

由於探長 R1 所執行的任務中用到 bankUser.income 變量,這裡的 . 符號其實就是 get() 操作;一旦涉及到 get() 操作,監督這個 income 屬性的觀察員 O1 就會執行 reportObserved 方法。該 reportObserved 方法對應的源碼如下:

reportObserved 方法的源碼

那麼多行代碼,我們主要關注其中操作影響到探長(derivation)中的操作:

  • 1. 更新探長的 lastAccessedBy 屬性(事務 id),這個是為了避免重複操作而設置的
  • 2. 更新探長的 newObserving 屬性,將探員信息推入到該隊列中(對應時序圖 (3.6.2)操作),這個比較重要,後續探長和觀察員更新依賴關係就靠這個屬性了

隨後,任務執行完(時序圖(3.7))後,探長就開始着手更新和觀察員 O1 的關聯關係了。?

2.2.3、bindDependencies

探長 R1 整理和觀察員的關係是在時序圖 (3.8)處:

探長 R1 整理和觀察員的關係

兩者依賴更新的算法在參考文章Mobx 源碼解讀(四) Reaction 中有詳細的註解,推薦閱讀。這裡也做一下簡單介紹。

該函數的目的,是用 derivation.newObserving 去更新 derivation.observing 屬性:

  • derivation.newObserving 就是剛才在所述時序圖 (3.6.2)操作是生成的
  • 執行完之後 derivation.newObserving 會置空,而 derivation.observing 屬性獲得更新,該屬性反映的 探長觀察員 之間最新的關聯關係;

依賴更新肯定需要遍歷,由於涉及到探長、觀察員兩個維度的數組,樸素算法的時間複雜度將是 O(n^2),而 MobX 中使用 3 次遍歷 + diffValue 屬性的輔助將複雜度降到了 O(n)

下面我用示例來展現這 3 次遍歷過程。

2.2.3.1、先看一下整體的 input / output

假設在執行 bindDependencies 函數之前, derivation.observing 已有 2 個元素,derivation.newObserving 有 5 個對象(由於 A、B 各重複一次,實際只有 3 個不同的對象 A、B、C),經過 bindDependencies 函數後 derivation.observing 將獲得更新,如下所示:

圖示bindDependencies

2.2.3.2、第一次循環:newObserving 數組去重

第一次循環遍歷 newObserving,利用 diffValue 進行去重,一次遍歷就完成了(這種 數組去重算法 可以添加到面試題庫中了??)。注意其中 diffValue 改變情況:

注意其中 diffValue 改變情況

由於 A 對象(引用)既在 observing 數組也在 newObserving 數組中,當改變 newObserving 中 A 元素的 diffValue 值的時候,observing 數組 A 屬性也自然跟着改變

這次遍歷後,所有 最新的依賴diffValue 值都是 1 了哦,而且去除了所有重複的依賴。

2.2.3.3、第二次循環:去除observing 數組陳舊關聯

接下去第二次遍歷針對 observing 數組,做了兩件事:

  • 如果對象的 diffValue 值為 0 (為 0 說明不在 newObserving 數組中,是陳舊的關聯),則調用 removeObserver 去除該關聯;因此這次遍歷之後會刪除 observing 數組中 D 對象
  • observing 數組中剩餘對象的 diffValue 值變成 0;

第二次遍歷

這一次遍歷之後,去除了所有陳舊的依賴,且遺留下來的對象的 diffValue 值都是 0 了。

2.2.3.4、第三次循環:將新增依賴添加到 observing

第二次遍歷針對 newObserving 數組,做了一件事:

  • 如果 diffValue 為 1,說明是新增的依賴,調用 addObserver 新增依賴,並將 diffValue 置為 0

調用 addObserver 新增依賴

這最後一次遍歷,observing 數組關聯都是最新,且 diffValue 都是 0 ,為下一次的 bindDependencies 做好了準備。

至此,A計劃部署方案(autorun 源碼)就講完了。A 計劃執行後,探長 R1 完成上級下達的任務,同時也和觀察員 O1 建立起明確且牢固的依賴

3、響應觀察值的變化 – propagateChanged

一旦張三存款發生變化,那麼一定會被觀察員 O1 監視到,請問此時觀察員會怎麼做?

或許有人會說,觀察員 O1 然後上報給探長 R1 ,然後讓探長 R1 再執行一次打印任務

從最終結果角度去理解,上面的陳述其實沒毛病,的確是觀察員 O1 驅動探長 R1 再打印一次

但若從執行過程角度去看,以上陳述是 錯誤的!?

觀察員 O1 監視到變化之後,的確通知探長 R1了,但探長並非直接執行任務,而是通知 MobX 再按照 A 計劃部署方案執行一遍!;(不得不感慨,這是多麼死板地執行機制)

源碼中是怎麼體現的呢?

上面提及到過,當觀察員所監控的值(比如income)發生變化時,會觸發 MobX 所提供的 propagateChanged 方法。

propagateChanged 對應的源碼如下:

propagateChanged 源碼

代碼很簡單,即遍歷觀察員的上級們,讓他們調用 onBecomeStale() 方法 。該觀察員有可能不止對一位上級(上級也不一定只有探長)負責,每位上級的 onBecomeStale() 是不一樣的。(當然,此故事中觀察員 O1 只有 1 位上級 —— 探長 R1)

我們看一下探長這類上級所定義的 onBecomeStale:

onBecomeStale() {  this.schedule()  }  

簡單明了,就是直接 再一次執行部署方案。如此簡單樸素,真正做到了 「一視同仁」 —— 無論什麼任務,一旦部署就緒,任何觀察員反饋情況有變(比如張三賬戶餘額發生變動了),探長都是讓 MobX 重新執行一遍部署方案,並不會直接執行任務,反正部署方案中有探長執行任務的步驟嘛。??

所謂的流程化、設計模式,都多多少少在一定程度上約束個體行為(喪失了一部分靈活性),而取得整體上的普適性和可擴展性

現在再回過頭來看剛才官方文檔截圖中的第二句話:"然後每次它的依賴關係改變時會再次被觸發"

理解官網對 autorun 的解釋

它所表達的意思其實就是:當張三餘額發生變化的時候,將 自動觸發 上述的 A 計劃部署方案。

4、小測試

問:下列代碼中 message.title = "Hello world" 為何不會觸發 autorun 再次執行?

const message = observable({ title: "hello" })    autorun(() => {  console.log(message)  })    // 不會觸發重新運行  message.title = "Hello world"  

其實上述問題來自官方的一個問題,若無思路的,可以先參考官方文檔 常見陷阱: console.log。如果能從源碼角度回答這個問題,則說明已經理解本節所講的 autorun 的知識點了

5、小結

此篇是開篇,所闡述的概念僅僅占 MobX 體系冰山一角。

冰山一角

故事中還還有很多問題,比如:

  1. 如何成為一名合格的探員、觀察員?(用程序員的話講,就是有哪些屬性和方法)
  2. 數據情報室到底還存有哪些關鍵信息?
  3. 組織機構中是否還有其他組、成員?
  4. 多個探長、觀察員情況下,這套部署方案又是如何的呢?
  5. ….

以上問題的答案,讀者可能已經知道,那些還不知道的可以自己留作自己思考;在後續章節,我也會在適當的時機解答上述中的問題; (也歡迎大家提問,將有可能採納編入後續的故事敘述中來解答)

後續的章節中,將繼續介紹 ComputedValueActionAtomDerivationSpy 等,正是這些功能角色使得 MobX 有着強大的自動化能力,合理運用了惰性求值、函數式編程等編程範式,使 MobX 在複雜交互應用中大放異彩;

REFERENCE

參考文檔

  • 官方中文文檔:不多講,官方文檔最好翻一遍。
  • awesome-mobx:MobX 相關資源整合,方便多看多練。
  • Mobx 源碼解讀(一) 基本概念:優質的 MobX 源碼解讀文章,受益匪淺。
  • MobX 核心源碼解析:本文深入 MobX 源碼來解析其核心原理以及工作流程,推薦閱讀;
  • 探秘 MobX:本文短小精悍,主講observableautorun 原理
  • MobX 原理:本文對 deviration 着墨較多