前言
我們在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)
}
複製程式碼
我們可以根據這三個屬性可以猜測出:
- name 應該是對這個一個簡單的命名
- delay 應該是延遲執行
- onError 應該是在autorun 方法執行報錯的時候執行的
以上只是根據程式碼猜測,我們接下來根據原始碼來具體分析這個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.
最終回撥derivation.js 的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 新增了值):
總結
- 執行autorun 方法,會產生一個Reaction 型別的物件
- 執行autorun 方法的回撥函式(引數),在這個函式裡面會引用我們 observable 物件的一些屬性,然後就會觸發對應的Proxy Get 方法
- 在get 方法裡, 會將對應的屬性裝飾過的ObservableValue 物件儲存到第一點中的Reaction 物件 的newObserving陣列中(如果在autorun回撥函式中,有引用兩個observable 屬性, 則 newObserving會有兩條記錄)
- 執行完回撥函式後,會去呼叫一個bindDependencies 方法, 回去遍歷newObserving陣列,將第一點中生成的Reaction 物件,儲存到每個屬性對應的ObservableValue 物件的 observers屬性中,如果一個屬性被多個autorun方法引用, 則observers屬性會儲存所有的Reaction 的物件(其實相當於觀察者模式中的所有的監聽者)
- 最終將observable 物件加工成了如下圖的物件
- 所以其實autorun 函式,是給上圖中的第三點中的observers 新增了值,也就是監聽者。
Todo
我們已經知道observable 物件和autorun 方法已經關聯起來,我們後續會繼續分析,當改變observable 屬性的值的時候,怎麼去觸發autorun 的回撥函式。我現在的猜想是:首先肯定會觸發Proxy 的set方法,然後set方法會遍歷呼叫observers 裡面的Reaction 的onInvalidate 方法,只是猜想,我們後面深入分析下。