Mobx 源码解析 二(autorun)
栏目: JavaScript · 发布时间: 6年前
内容简介:我们在在Git 上面创建了一个新的可以看出,我们给autorun 方法传递了第二个参数, 而且是一个Object :
我们在 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 添加了值):
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- ReactNative源码解析-初识源码
- Spring源码系列:BeanDefinition源码解析
- Spring源码分析:AOP源码解析(下篇)
- Spring源码分析:AOP源码解析(上篇)
- 注册中心 Eureka 源码解析 —— EndPoint 与 解析器
- 新一代Json解析库Moshi源码解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
REST实战
Jim Webber、Savas Parastatidis、Ian Robinson / 李锟、俞黎敏、马钧、崔毅 / 东南大学出版社 / 2011-10 / 78.00元
为何典型的企业项目无法像你为web所开发的项目那样运行得如此平滑?对于建造分布式和企业级的应用来说,rest架构风格真的提供了一个可行的替代选择吗? 在这本富有洞察力的书中,三位soa专家对于rest进行了讲求实际的解释,并且通过将web的指导原理应用到普通的企业计算问题中,向你展示了如何开发简单的、优雅的分布式超媒体系统。你将会学习到很多技术,并且随着一家典型的公司从最初的小企业逐渐成长为......一起来看看 《REST实战》 这本书的介绍吧!