Mobx 原始碼解析 二(autorun)
我們在 ofollow,noindex">Mobx 原始碼解析 一(observable) 已經知道了 observable 做的事情了, 但是我們的還是沒有講解明白在我們的 Demo 中,我們在 Button
的 Click
事件中只是對 bankUser.income
進行了自增和自減,並沒有對 incomeLabel
進行操作, 但是 incomeLabel
的內容卻實時的更新了, 我們分析只有在 mobx.autorun
方法中對其的 innerText
進行了處理, 所以很容易理解神祕之處在於此方法,接下來我們來深入分析這個方法的實現原理.
Demo
在Git 上面建立了一個新的 autorun 分支, 對Demo 的程式碼進行小的變更,變更的主要是autorun 方法:
const incomeDisposer = mobx.autorun(() => { if (bankUser.income < 0) { bankUser.income = 0 throw new Error('throw new error') } incomeLabel.innerText = `Ivan Fan income is ${bankUser.income}` }, { name: 'income', delay: 2*1000, onError: (e) => { console.log(e) } }) 複製程式碼
可以看出,我們給autorun 方法傳遞了第二個引數, 而且是一個Object :
{ name: 'income', delay: 2*1000, onError: (e) => { console.log(e) } 複製程式碼
我們可以根據這三個屬性可以猜測出:
autorun
autorun
autorun原始碼如下:
export function autorun(view, opts = EMPTY_OBJECT) { if (process.env.NODE_ENV !== "production") { invariant(typeof view === "function", "Autorun expects a function as first argument"); invariant(isAction(view) === false, "Autorun does not accept actions since actions are untrackable"); } const name = (opts && opts.name) || view.name || "Autorun@" + getNextId(); const runSync = !opts.scheduler && !opts.delay; let reaction; if (runSync) { // normal autorun reaction = new Reaction(name, function () { this.track(reactionRunner); }, opts.onError); } else { const scheduler = createSchedulerFromOptions(opts); // debounced autorun let isScheduled = false; reaction = new Reaction(name, () => { if (!isScheduled) { isScheduled = true; scheduler(() => { isScheduled = false; if (!reaction.isDisposed) reaction.track(reactionRunner); }); } }, opts.onError); } function reactionRunner() { view(reaction); } reaction.schedule(); return reaction.getDisposer(); } 複製程式碼
檢視這個方法,發現其可以傳遞兩個引數:
- view, 必須是一個function, 也就是我們要執行的業務邏輯的地方.
- opts, 是一個可選引數, 而且是一個Object, 可以傳遞的屬性有四個
name
,scheduler
,delay
,onError
, 其中delay和scheduler 是比較重要的兩個引數,因為決定是否同步還是非同步. - 檢視這個方法的最後第二行
reaction.schedule();
, 其實表示已經在autorun 方法呼叫時,會立即執行一次其對應的回撥函式
同步處理
在上面的梳理中發現, 如果傳遞了 delay
或者 scheduler
值,其進入的是 else
邏輯分支,也就是非同步處理分支,我們現在先將 demo 中的 delay: 2*1000,
屬性給註釋, 先分析同步處理的邏輯( normal autorun 正常的autorun)
建立reaction(反應)例項
首先建立了一個Reaction是例項物件,其中傳遞了兩個引數: name 和一函式, 這個函式掛載在一個叫 onInvalidate
屬性上,這個函式最終會執行我們的 autorun
方法的第一個引數 viwe
, 也就是我們要執行的業務邏輯程式碼:
reaction = new Reaction(name, function () { this.track(reactionRunner); }, opts.onError); 複製程式碼
function reactionRunner() { view(reaction); } 複製程式碼
呼叫reaction.schedule()方法
我們看到,例項化 reaction
物件後,立即執行了其 schedule
方法,然後就只是返回一個物件 reaction.getDisposer()
物件, 整個 autorun
方法就結束了。
autorun
方法看起來很簡單,但是為什麼能在其對應的屬性變更時,就立即執行 view
方法呢, 其奧妙應該在於 schedule
方法中,所以我們應該進一步分析這個方法.
schedule() { if (!this._isScheduled) { this._isScheduled = true; globalState.pendingReactions.push(this); runReactions(); } } 複製程式碼
- 設定一個標識:_isScheduled = true, 表示當前例項已經在安排中
-
globalState.pendingReactions.push(this);
將當前例項放在一個全域性的陣列中globalState.pendingReactions
- 執行runReactions 方法.
runReactions 方法(執行所有的reaction)
const MAX_REACTION_ITERATIONS = 100; let reactionScheduler = f => f(); export function runReactions() { if (globalState.inBatch > 0 || globalState.isRunningReactions) return; reactionScheduler(runReactionsHelper); } function runReactionsHelper() { globalState.isRunningReactions = true; const allReactions = globalState.pendingReactions; let iterations = 0; while (allReactions.length > 0) { if (++iterations === MAX_REACTION_ITERATIONS) { allReactions.splice(0); // clear reactions } let remainingReactions = allReactions.splice(0); for (let i = 0, l = remainingReactions.length; i < l; i++) remainingReactions[i].runReaction(); } globalState.isRunningReactions = false; } 複製程式碼
- 判斷全域性變數
globalState.inBatch > 0 || globalState.isRunningReactions
是否有在執行的reaction. - 執行runReactionsHelper() 方法
- 設定 globalState.isRunningReactions = true;
- 獲取所有等待中的reaction,
const allReactions = globalState.pendingReactions;
(我們在schedule
方法分析中,在這個方法,將每一個reaction 例項放到這個globalState 陣列中) - 遍歷所有等待中的reaction 然後去執行
runReaction
方法(remainingReactions[i].runReaction();
) - 最後將
globalState.isRunningReactions = false;
這樣就可以保證一次只有一個autorun
在執行,保證了資料的正確性
我們分析了基本流程,最終執行的是在 Reaction 例項方法 runReaction
方法中,我們現在開始分析這個方法。
runReaction 方法(真正執行autorun 中的業務邏輯)
runReaction() { if (!this.isDisposed) { startBatch(); this._isScheduled = false; if (shouldCompute(this)) { this._isTrackPending = true; try { this.onInvalidate(); if (this._isTrackPending && isSpyEnabled() && process.env.NODE_ENV !== "production") { spyReport({ name: this.name, type: "scheduled-reaction" }); } } catch (e) { this.reportExceptionInDerivation(e); } } endBatch(); } } 複製程式碼
-
startBatch();
只是設定了globalState.inBatch++;
-
this.onInvalidate();
關鍵是這個方法, 這個方法是例項化 Reaction 物件傳遞進來的,其最終程式碼如下:
reaction = new Reaction(name, function () { this.track(reactionRunner); }, opts.onError); 複製程式碼
function reactionRunner() { view(reaction); } 複製程式碼
所以 this.onInvalidate
其實就是:
function () { this.track(reactionRunner); } 複製程式碼
如何和observable 處理過的物件關聯?
上面我們已經分析了autorun 的基本執行邏輯, 我們可以在 this.track(reactionRunner);
地方,打個斷點, 檢視下function 的call stack.

的trackDerivedFunction 方法, 這個方法有三個引數:
- derivation,就是autorun 方法建立的 Reaction 例項
- f, 就是autorun的回撥函式, 也就是derivation的onInvalidate 屬性
我們檢視到 result = f.call(context);
,很明顯這個地方是就是執行autorun方法回撥函式的地方。
我們看到在這個方法中將當前的 derivation
賦值給了 globalState.trackingDerivation = derivation;
,這個值在其他的地方會呼叫。 我們再回過頭來看下 autorun 的回撥函式到底是個什麼:
const incomeDisposer = autorun((reaction) => { incomeLabel.innerText = `${bankUser.name} income is ${bankUser.income}` }) 複製程式碼
在這裡,我們呼叫了 bankUser.name
, bankUser.income
,其中 bankUser 是一個被 observable 處理的物件,我們在 Mobx 原始碼解析 一(observable) 中知道, 這個物件用 Proxy 進行了代理, 我們讀取他的任何屬性,都會鍵入攔截器的 get 方法,我們接下來分析下 get 方法到底做了什麼。
Proxy get 方法
get 方法的程式碼如下:
get(target, name) { if (name === $mobx || name === "constructor" || name === mobxDidRunLazyInitializersSymbol) return target[name]; const adm = getAdm(target); const observable = adm.values.get(name); if (observable instanceof Atom) { return observable.get(); } if (typeof name === "string") adm.has(name); return target[name]; } 複製程式碼
在 Mobx 原始碼解析 一(observable) 中我們知道,observable 是一個ObservableValue 型別, 而ObservableValue 又繼承與Atom, 所以程式碼會走如下分支:
if (observable instanceof Atom) { return observable.get(); } 複製程式碼
我們繼續檢視其對應的get 方法
get() { this.reportObserved(); return this.dehanceValue(this.value); } 複製程式碼
這裡有一個關鍵的方法:this.reportObserved();, 顧名思義,就是我要報告我要被觀察了,將 observable 物件和 autorun 方法給關聯起來了,我們可以繼續跟進這個方法。
通過斷點,我們發現,最終會呼叫 observable.js 的reportObserved方法。

其方法的具體程式碼如下,我們會一行行的進行分析
export function reportObserved(observable) { const derivation = globalState.trackingDerivation; if (derivation !== null) { if (derivation.runId !== observable.lastAccessedBy) { observable.lastAccessedBy = derivation.runId; derivation.newObserving[derivation.unboundDepsCount++] = observable; if (!observable.isBeingObserved) { observable.isBeingObserved = true; observable.onBecomeObserved(); } } return true; } else if (observable.observers.size === 0 && globalState.inBatch > 0) { queueForUnobservation(observable); } return false; } 複製程式碼
- 引數:observable 是一個 ObservableValue 物件, 在第一章節的分析,我們已經知道經過observable 加工過的物件,每個屬性被加工這個型別的物件,所以這個物件,也就是對應的屬性。
- 第二行
const derivation = globalState.trackingDerivation;
這行程式碼和容易理解,就是從globalstate 取一個值,但是這個值的來源很重要, 上面我們在 derivation.js 的trackDerivedFunction 方法中,發現對其賦值了globalState.trackingDerivation = derivation;
。而其對應的值derivation
就是對應的 autorun 建立的 Reaction 物件 -
derivation.newObserving[derivation.unboundDepsCount++] = observable;
這一行至關重要, 將observable物件的屬性和autorun 方法真正關聯了。
在我們的 autorun 方法中呼叫了兩個屬性,所以在執行兩次 get 方法後,對應的 globalState.trackingDerivation 值如下圖所示:

其中 newObserving 屬性中,有了兩個值,著兩個值,表示當前的這個autorun 方法,會監聽這個兩個屬性,我們接下來會解析,怎麼去處理 newObserving 陣列
我們繼續來分析trackDerivedFunction 方法
export function trackDerivedFunction(derivation, f, context) { changeDependenciesStateTo0(derivation); derivation.newObserving = new Array(derivation.observing.length + 100); derivation.unboundDepsCount = 0; derivation.runId = ++globalState.runId; const prevTracking = globalState.trackingDerivation; globalState.trackingDerivation = derivation; let result; if (globalState.disableErrorBoundaries === true) { result = f.call(context); } else { try { result = f.call(context); } catch (e) { result = new CaughtException(e); } } globalState.trackingDerivation = prevTracking; bindDependencies(derivation); return result; } 複製程式碼
上面我們已經分析完了 result = f.call(context);
這一步驟, 我們現在要分析: bindDependencies(derivation);方法
bindDependencies 方法
引數 derivation ,在執行每個屬性的 get 方法時, 已經給 derivatio 的 newObserving 屬性添加了兩條記錄, 如圖:

我們接下來深入分析 bindDependencies 方法,發現其對 newObserving 進行了遍歷處理,如下
while (i0--) { const dep = observing[i0]; if (dep.diffValue === 1) { dep.diffValue = 0; addObserver(dep, derivation); } } 複製程式碼
addObserver(dep, derivation);
,由方法名猜想,這個應該是去新增觀察了,我們檢視下具體程式碼:
export function addObserver(observable, node) { observable.observers.add(node); if (observable.lowestObserverState > node.dependenciesState) observable.lowestObserverState = node.dependenciesState; } 複製程式碼
引數: observable 就是我們每個屬性對應的 ObservableValue , 有一個 Set 型別的observers 屬性 , node就是我們autorun 方法建立的 Reaction 物件
observable.observers.add(node); 就是每個屬性儲存了其對應的觀察者。
其最終將 observable 的物件加工成如下圖所示(給第三步的observes 添加了值):
