Mobx 源碼解析 二(autorun)

前言

咱們在Mobx 源碼解析 一(observable)已經知道了observable 作的事情了, 可是咱們的仍是沒有講解明白在咱們的Demo中,咱們在ButtonClick 事件中只是對bankUser.income 進行了自增和自減,並無對incomeLabel進行操做, 可是incomeLabel 的內容卻實時的更新了, 咱們分析只有在mobx.autorun 方法中對其的innerText 進行了處理, 因此很容易理解神祕之處在於此方法,接下來咱們來深刻分析這個方法的實現原理.node

Demo

在Git 上面建立了一個新的autorun分支, 對Demo 的代碼進行小的變動,變動的主要是autorun 方法:react

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 :git

{
    name: 'income',
    delay: 2*1000,
    onError: (e) => {
        console.log(e)
    }
複製代碼

咱們能夠根據這三個屬性能夠猜想出:github

  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();
}
複製代碼

查看這個方法,發現其能夠傳遞兩個參數:bash

  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方法就結束了。post

autorun 方法看起來很簡單,可是爲何能在其對應的屬性變動時,就當即執行view方法呢, 其奧妙應該在於schedule 方法中,因此咱們應該進一步分析這個方法.ui

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.

最終回調 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方法。

其方法的具體代碼以下,咱們會一行行的進行分析

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值以下圖所示:

其中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 屬性添加了兩條記錄, 如圖:

咱們接下來深刻分析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 添加了值):

總結

  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 對象加工成了以下圖的對象
  6. 因此其實autorun 函數,是給上圖中的第三點中的observers 添加了值,也就是監聽者。

Todo

咱們已經知道observable 對象和autorun 方法已經關聯起來,咱們後續會繼續分析,當改變observable 屬性的值的時候,怎麼去觸發autorun 的回調函數。我如今的猜測是:首先確定會觸發Proxy 的set方法,而後set方法會遍歷調用observers 裏面的ReactiononInvalidate 方法,只是猜測,咱們後面深刻分析下。

相關文章
相關標籤/搜索