Mobx 源码解析 二(autorun)
前言
我们在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 = 0throw 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 autorunreaction = new Reaction(name, function () {this.track(reactionRunner);}, opts.onError);}else {const scheduler = createSchedulerFromOptions(opts);// debounced autorunlet 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 方法,只是猜想,我们后面深入分析下。
作者:bluebrid
链接:https://juejin.im/post/5b98cc896fb9a05cf039cd11
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Mobx 源码解析 二(autorun)相关推荐
- 【深度学习模型】智云视图中文车牌识别源码解析(二)
[深度学习模型]智云视图中文车牌识别源码解析(二) 感受 HyperLPR可以识别多种中文车牌包括白牌,新能源车牌,使馆车牌,教练车牌,武警车牌等. 代码不可谓不混乱(别忘了这是职业公司的准产品级代码 ...
- erlang下lists模块sort(排序)方法源码解析(二)
上接erlang下lists模块sort(排序)方法源码解析(一),到目前为止,list列表已经被分割成N个列表,而且每个列表的元素是有序的(从大到小) 下面我们重点来看看mergel和rmergel ...
- Kubernetes学习笔记之Calico CNI Plugin源码解析(二)
女主宣言 今天小编继续为大家分享Kubernetes Calico CNI Plugin学习笔记,希望能对大家有所帮助. PS:丰富的一线技术.多元化的表现形式,尽在"360云计算" ...
- android网络框架retrofit源码解析二
注:源码解析文章参考了该博客:http://www.2cto.com/kf/201405/305248.html 前一篇文章讲解了retrofit的annotation,既然定义了,那么就应该有解析的 ...
- Thrift源码解析(二)序列化协议
概述 对于一个RPC框架,定义好网络数据的序列化协议是最基本的工作,thrift的序列化协议主要包含如下几种: TBinaryProtocol TCompactProtocol TJSONProtoc ...
- matlabeig函数根据什么原理_vue3.0 源码解析二 :响应式原理(下)
一 回顾上文 上节我们讲了数据绑定proxy原理,vue3.0用到的基本的拦截器,以及reactive入口等等.调用reactive建立响应式,首先通过判断数据类型来确定使用的hander,然后创建p ...
- Spring源码解析二之创建Bean(实例化)
上一节我们分析到了createBean,而真正创建Bean的过程在doCreateBean过程,我们可以发现Spring的编码风格,do才是真正的过程,不带do的通常是在做在准备过程,并且我们跳过了一 ...
- 第37篇 Asp.Net源码解析(二)--详解HttpApplication
这篇文章花了点时间,差点成烂到电脑里面,写的过程中有好几次修改,最终的这个版本也不是很满意,东西说的不够细,还需要认真的去看下源码才能有所体会,先这样吧,后面有时间把细节慢慢的再修改.顺便对于开发的学 ...
- Spring Cloud微服务系列-Eureka Client源码解析(二)
导语 上一篇博客中介绍了关于Eureka Client源码的基础部分,如果对于基础部分不是很了解的读者可以点击下面的连接进入到源码分析一中,从头开始学习 Spring Cloud微服务系列 Dis ...
最新文章
- 小型软件项目开发流程探讨
- Dataset:Big Mart Sales数据集的简介、下载、案例应用之详细攻略
- 微信小程序直播自己的服务器,使用微信小程序和腾讯云实现直播功能
- 【限时早鸟票】数据技术十年相伴,DTC盛会北京重燃
- python RandomTrees特征编码
- 2019级C语言大作业 - 十步万度
- vcglib中面自相交的检测算法
- 系统学习深度学习(四十三)--GAN简单了解
- 实时系统vxWorks - 配置多网口
- CANopen协议 学习笔记
- Excel如何将某个特定值变为空值
- 股价大跌、现金流承压,工业富联风光不再?
- 出口路由器网关配置案例
- 计算机毕业后的打算英语作文,毕业后的打算高中英语作文
- 使用tensorboard时http://localhost:6006打不开或desktop-2a1fhsu 已拒绝连接
- stm32利用外部中断控制小台灯
- 很抱歉,此功能看似已中断,并需要修复。请使用Windows控制面板中的“程序和功能”选项修复Microsoft Office。
- 阴阳师系统转移开放服务器,阴阳师:运营商倒闭后?原来只是子账号数据转移,可迁移到官服...
- 图片滤镜——GPUImage
- 全国电子联行系统(EIS)、大额支付系统、
热门文章
- birthday中文是什么_birthday什么意思中文翻译
- 阿里云国际站:实名认证上传材料填写样例(域名持有者为个人)
- 我所理解的设计模式——对象行为之命令(Comand)模式
- Kubernetes之Ingress
- 人机融合智能中的计算-算计问题
- 【剑侠情缘服务端】武侠题材角色扮演类手游源码+手工外网端+安卓APP+视频教程
- MATLAB车牌识别(含GUI,语音播报)
- 《MT6582平台上调试耳机插尽后无响应问题》
- 天天向上python流程图_每天一遍,好好学习,天天向上(Python)
- 智能手机发布会上云,是“迫不得已”还是“刻不容缓”?