Mobx 源码解析 二(autorun)

栏目: JavaScript · 发布时间: 6年前

内容简介:我们在在Git 上面创建了一个新的可以看出,我们给autorun 方法传递了第二个参数, 而且是一个Object :

我们在 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)
    }
复制代码

我们可以根据这三个属性可以猜测出:

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)

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

REST实战

REST实战

Jim Webber、Savas Parastatidis、Ian Robinson / 李锟、俞黎敏、马钧、崔毅 / 东南大学出版社 / 2011-10 / 78.00元

为何典型的企业项目无法像你为web所开发的项目那样运行得如此平滑?对于建造分布式和企业级的应用来说,rest架构风格真的提供了一个可行的替代选择吗? 在这本富有洞察力的书中,三位soa专家对于rest进行了讲求实际的解释,并且通过将web的指导原理应用到普通的企业计算问题中,向你展示了如何开发简单的、优雅的分布式超媒体系统。你将会学习到很多技术,并且随着一家典型的公司从最初的小企业逐渐成长为......一起来看看 《REST实战》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

MD5 加密
MD5 加密

MD5 加密工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具