Mobx 原始碼解析 二(autorun)

bluebrid發表於2019-03-04

前言

我們在Mobx 原始碼解析 一(observable)已經知道了observable 做的事情了, 但是我們的還是沒有講解明白在我們的Demo中,我們在ButtonClick 事件中只是對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)
    }
複製程式碼

我們可以根據這三個屬性可以猜測出:

  1. name 應該是對這個一個簡單的命名
  2. delay 應該是延遲執行
  3. 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();
}
複製程式碼

檢視這個方法,發現其可以傳遞兩個引數:

  1. view, 必須是一個function, 也就是我們要執行的業務邏輯的地方.
  1. opts, 是一個可選引數, 而且是一個Object, 可以傳遞的屬性有四個name, scheduler, delay, onError, 其中delay和scheduler 是比較重要的兩個引數,因為決定是否同步還是非同步.
  2. 檢視這個方法的最後第二行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();
        }
    }
複製程式碼
  1. 設定一個標識:_isScheduled = true, 表示當前例項已經在安排中
  2. globalState.pendingReactions.push(this); 將當前例項放在一個全域性的陣列中globalState.pendingReactions
  3. 執行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;
}
複製程式碼
  1. 判斷全域性變數globalState.inBatch > 0 || globalState.isRunningReactions 是否有在執行的reaction.
  2. 執行runReactionsHelper() 方法
  3. 設定 globalState.isRunningReactions = true;
  4. 獲取所有等待中的reaction, const allReactions = globalState.pendingReactions;(我們在schedule 方法分析中,在這個方法,將每一個reaction 例項放到這個globalState 陣列中)
  5. 遍歷所有等待中的reaction 然後去執行runReaction 方法( remainingReactions[i].runReaction();)
  6. 最後將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();
        }
    }
複製程式碼
  1. startBatch(); 只是設定了globalState.inBatch++;
  2. 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.

Mobx 原始碼解析 二(autorun)

最終回撥derivation.js 的trackDerivedFunction 方法, 這個方法有三個引數:

  1. derivation,就是autorun 方法建立的Reaction 例項
  1. 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方法。

Mobx 原始碼解析 二(autorun)

其方法的具體程式碼如下,我們會一行行的進行分析

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;
}
複製程式碼
  1. 引數:observable 是一個ObservableValue 物件, 在第一章節的分析,我們已經知道經過observable 加工過的物件,每個屬性被加工這個型別的物件,所以這個物件,也就是對應的屬性。
  2. 第二行const derivation = globalState.trackingDerivation;這行程式碼和容易理解,就是從globalstate 取一個值,但是這個值的來源很重要, 上面我們在derivation.js 的trackDerivedFunction 方法中,發現對其賦值了globalState.trackingDerivation = derivation;。而其對應的值derivation就是對應的autorun 建立的Reaction 物件
  3. derivation.newObserving[derivation.unboundDepsCount++] = observable; 這一行至關重要, 將observable物件的屬性和autorun 方法真正關聯了。

在我們的autorun 方法中呼叫了兩個屬性,所以在執行兩次get 方法後,對應的globalState.trackingDerivation值如下圖所示:

Mobx 原始碼解析 二(autorun)

其中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 方法時, 已經給derivationewObserving 屬性新增了兩條記錄, 如圖:

Mobx 原始碼解析 二(autorun)

我們接下來深入分析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 新增了值):

Mobx 原始碼解析 二(autorun)

總結

  1. 執行autorun 方法,會產生一個Reaction 型別的物件
  2. 執行autorun 方法的回撥函式(引數),在這個函式裡面會引用我們 observable 物件的一些屬性,然後就會觸發對應的Proxy Get 方法
  3. 在get 方法裡, 會將對應的屬性裝飾過的ObservableValue 物件儲存到第一點中的Reaction 物件 的newObserving陣列中(如果在autorun回撥函式中,有引用兩個observable 屬性, 則 newObserving會有兩條記錄)
  4. 執行完回撥函式後,會去呼叫一個bindDependencies 方法, 回去遍歷newObserving陣列,將第一點中生成的Reaction 物件,儲存到每個屬性對應的ObservableValue 物件的 observers屬性中,如果一個屬性被多個autorun方法引用, 則observers屬性會儲存所有的Reaction 的物件(其實相當於觀察者模式中的所有的監聽者)
  5. 最終將observable 物件加工成了如下圖的物件
    Mobx 原始碼解析 二(autorun)
  6. 所以其實autorun 函式,是給上圖中的第三點中的observers 新增了值,也就是監聽者。

Todo

我們已經知道observable 物件和autorun 方法已經關聯起來,我們後續會繼續分析,當改變observable 屬性的值的時候,怎麼去觸發autorun 的回撥函式。我現在的猜想是:首先肯定會觸發Proxy 的set方法,然後set方法會遍歷呼叫observers 裡面的ReactiononInvalidate 方法,只是猜想,我們後面深入分析下。

相關文章