================前言===================javascript
本系列文章:html
=======================================java
在写本文的时候,因为 MobX 以及升级到 4.x,API 有较大的变化,所以后续的文章默认都将基于 4.x 以上版本进行源码阅读。react
前一篇文章仍然以 mobx v3.5.1 的源码,autorun
逻辑在新版中没有更改,所以源码逻辑仍旧一致。git
为了多维度掌控嫌疑犯的犯罪特征数据,你(警署最高长官)想要获取并实时监控张三的 贷款数额、存贷比(存款和贷款二者比率) 的变化。github
因而你就拟定了新的命令给执行官 MobX:segmentfault
var bankUser = mobx.observable({ income: 3, debit: 2 }); var divisor = mobx.computed(() => { return bankUser.income / bankUser.debit; }); mobx.autorun(() => { console.log('张三的贷款:', bankUser.debit, ';张三的存贷比: ' + divisor); });
相比上一次的命令,除了监控张三贷款这项直接的指标,还须要监控 贷款比(divisor
) 这项间接指标。性能优化
执行官 MobX 稍做思忖,要完成这个任务比以前的要难一点点,须要费一点儿精力。微信
不过,这也难不倒能力强大的 MobX 执行官,一番策略调整以后,从新拿出新的执行方案。部署实施以后,当张三去银行存款、贷款后,这些变化都实时反馈出来了:ide
此次的部署和前一次相差不大,除了须要让观察员 O2(监视 income
)参与进来以外,考虑到警署最高长官所需的 存贷比 (divisor
),还得派出另外一类职员 —— 会计师:
会计师是一个颇有意思的角色,要想理解他们,必须得思考他们的数据“从哪儿来?到哪里去?” 这两个问题:
引入了会计师角色以后,MobX 执行官从新绘制了部署计划图:
解释一下此计划图的意思:
debit
)和存贷比(divisor
):() => { console.log('张三的贷款:', bankUser.debit, ';张三的存贷比: ' + divisor); }
bankUser.income
属性和 bankUser.debit
属性;由于仍是 autorun
命令,因此仍然执行 A计划方案(详情参考上一篇《【用故事解读 MobX源码(一)】 autorun》)MobX 执行官的部署方案从总体上看是同样的,考虑到多了会计师这个角色的参与,因此特地在探长 获取存贷比(divisor
) 逻辑处空出一部分留给会计师让它自由发挥:
这样作,MobX 执行官也为了在实际行动中向他的警署长官证明该 A计划方案 的确拥有“良好的扩展性”。
解开这层新增的会计师计算逻辑 “面纱”,图示以下:
你会发现历史老是惊人的类似,新增的会计师执行计算任务的逻辑其实 探长 执行任务的逻辑是同样的,下图中我特地用 相同的序号(不一样的颜色形状)标示 出,序号所对应含义以下:
此执行计算任务的逻辑,若是不告诉观察员的话,观察员还觉得又来了一名“探长”上级。?
从部署图里咱们能够看出会计师具备两面性;
自从有了会计师的参与,探长仍是那个探长,但他的下级已经不是以前的下级了。借助 A计划任务的执行,会计师 C1 在上报计算值的时候,会顺水推舟地执行计算任务,同时更新他的 ”关系网“。
会计师有一个特性就是比较懒:就算观察员所观察到的值变动了,他们也不会当即从新计算,而只在必要的时候(好比当上级前来索取时)才会从新计算。
举个例子,当观察员 O1 发现张三的帐户存款从原来的 3 变成 6 :
bankUser.income = 6;
这个时候会触发一系列的 “涟漪”:
income
)有变动将上面的文字转换成流程图,能够清晰看到各角色在此次“涟漪”中所起到的做用:
这里须要注意 3 点:
会计师这种拖延到 只有被须要的时候才进行计算 的行为,有没有让你回忆起学生时代寒假结束前一天疯狂补做业的场景??
当执行官 MobX 拿着这份执行报告送达给你(警署最高长官),阅览完毕:”不错,这套方案的确部分证明了你以前所言的可扩展性。但随着职员的引入,运起色构逐渐庞大,如何避免没必要要的开销的呢?“
”长官您高瞻远瞩,这的确是一个问题。在井井有理的规则下,个别职员的运做效率的确会打折扣。所以避免职员没必要要的计算开销,也是在我方案部署规划以内。正如您所见,上述方案中会计师的‘惰性’、探员在事务以后再进行任务等机制,都是基于优化性能所采起的措施。“ 执行官 MobX 稍做停顿,继续道,”为了更好地阐述这套运行方案的性能优化机制,我明天呈上一份报告,好让您得以全面了解。“
”Good Job!期待你的报告“。
那么,执行官 MobX 是凭借什么机制减小开销的呢?且听下回分解。
(本节完,未完待续)
本节部分,仍然是就着上面的”故事“来说 MobX 中的源码。
先罗列本文故事中新出现的 会计师 角色与 MobX 源码概念映射关系:
故事人物 | MobX 源码 | 解释 |
---|---|---|
会计师 | computedvalue | 官方文档 - (@)computed 计算值 |
探长、执行官等角色的映射关系,参考上一篇《 【用故事解读 MobX源码(一)】 autorun》
本文的重点内容就是 computedvalue 的部分源码(它在 autorun
等场景中的应用)
autorun
(A 计划)的源码在上一节讲过,这里再也不赘述。咱们仅仅讲解一下 computedValue 在 autorun
中的表现。
在故事中咱们讲到过,当探长向会计师索要计算值的时候,此时懒惰的会计师为了 ”应付交差“,这时候才开始计算,其计算的过程和探长执行的任务流程几乎一致。
从源码角度去看一下其中的缘由。
当探长执行任务:
() => { console.log('张三的贷款:', bankUser.debit, ';张三的存贷比: ' + divisor); }
任务中也涉及 bankUser.debit
变量和 divisor
变量;其中在获取 bankUser.debit
变量之时会让观察员 O2 触发 reportObserved
方法,这个上一篇文章着重讲过,此处就不详细展开了;而请求 divisor
数值的时候,则会触发该值的 valueOf()
方法 —— 即调用会计师(computedValue)的 valueOf()
方法。
为何调用就触发 valueOf()
方法呢?请看下方的“知识点”备注?
======== 插播知识点 =========任何原始值仍是对象其实都包含
valueOf()
或toString()
方法,valueOf()
会返回最适合该对象类型的原始值,toString()
将该对象的原始值以字符串形式返回。
这两个方法通常是交由 JS 去隐式调用,以知足不一样的运算状况。好比在数值运算(如a + b
)里会优先调用valueOf()
,而在字符串运算(如alert(c
))里,会优先调用toString()
方法
顺带附上两篇 参考文章
======== 完毕 ==========
一旦调用调用会计师的 valueOf 方法:
valueOf(): T { return toPrimitive(this.get()) }
其实就是调用 this.get() 方法,咱们瞧一眼源码;
这里有个分叉点,根据 globalState.inBatch
决定究竟是启用 重量级计算 仍是 轻量级计算:
globalState.inBatch
值大于 0,说明会计师被上级征调(处于上级事务中),好比此案例中,陷于 A 计划(autorun
)的会计师,在上级探长 R1 须要查阅计算值时候,就会进入重量级计算模式globalState.inBatch
值为 0,就会进入轻量级计算模式,简化计算的逻辑。但不管轻量级仍是重量级计算,都会涉及到调用 computeValue() 方法来执行计算任务。
调用的时候,若是是 重量级计算 则 track
这个 bool 值为 true,不然track
值为 false。
计算值有个属性,this.derivation
就是会计师要计算数值时所依据的计算表达式,也就是而咱们定义会计师时所传入的匿名函数:
() => { return bankUser.income / bankUser.debit; }
不管是 重量级计算 模式仍是 轻量级计算 模式,最终都是会调用该计算表达式获取计算值。
重量级计算 模式和 轻量级计算 模式二者的差异只是在于前者在执行该计算表达式以前会设置不少环境,后者直接就按这个表达式计算数值返回。
在上述的故事中,因为探长 R1 人物的存在,会计师会执行 重量级计算 模式,接下来的源码分析也走这条分支路线。( 轻量级计算 模式的状况当作课后思考题)。
在 重量级计算的时候,computeValue(true)
就会走和 探长 操做模式同样 trackDerivedFunction
步骤。没错,探长和会计师调用的就是同一个方法,因此他们在执行任务的时候,行为痕迹是同样的,没毛病。
若是忘记
trackDerivedFunction
方法内容,请查看 《【用故事解读 MobX源码(一)】 autorun》的 ”2.2.二、trackDerivedFunction“ 部分
只不过会计师只能执行计算类的任务(纯函数)罢了,探长能够执行任意类型的任务。
和探长同样,会计师执行计算任务完毕以后调用 bindDependencies
将绑定 观察员 O1 和 观察员 O2 ;而在执行计算以后,会计师会调用 propagateChangeConfirmed
方法,更改本身和上级 探长 的状态 —— 这说明,对探长而言,会计师就至关于 观察员的角色,在探长执行任务结束后像观察员同样须要上报本身的计算值,并和 探长 取得联系;
这么看会计师还真 ”墙头草,两边倒”。
至此,会计师这个角色以较低的成本就能完美地整合进执行官 MobX 所部署的 A 集合部署方案中。??
一旦张三的帐户存款(income
)发生变化,将会触发 MobX 所提供的 reportChanged 方法:
public reportChanged() { startBatch() propagateChanged(this) endBatch() }
注意这里的startBatch
和endBatch
方法,说明观察员 O1 发起事务了。
咱们知道(不知道的请阅读上一篇文章)该 reportChanged()
方法中的 propagateChanged()
会触发上级的 onBecomeStale()
方法。
观察员 O1 此时的上级是 会计师 C1,其所定义的 onBecomeStale 以下:
onBecomeStale() { propagateMaybeChanged(this) }
看一下 propagateMaybeChanged(this) 源码,也比较简单,主要作了两件事情,① 会计师会调整自身的状态; ②而后触发其上级(探长 R1)的 onBecomeStale()
方法。
可见观察员 01 会引发会计师 C1 的响应,而会计师会引发探长 R1 的响应,这种响应“涟漪”就是经过下级触发上级的 onBecomeStale
方法造成的连锁反应。
不一样上级(好比会计师和探长)的
onBecomeStale
定义不一样。
探长的这个 onBecomeStale
方法在上一篇文章的 “三、响应观察值的变化 - propagateChanged” 中咱们讲过,探长将请求 MobX 请求从新执行一遍 A 计划方案。
然而,MobX 拒绝了此次请求,让他再等待一下。??
这是由于在 runReactions 方法中:
if (globalState.inBatch > 0 || globalState.isRunningReactions) return
因为此时 inBatch
是 1(由于观察员执行了 startBatch()
),因此会直接 return 掉。
直到观察员执行 endBatch() 的时候,除了会结束本次的上报事务,同时执行官 MobX 会从新执行 runReactions
方法,让久等的探长去执行任务:
探长在执行任务的时候,就会打印张三的贷款(debit
)、存贷比(divisor
)了。
综上,当张三存款(income
)变动,就能让 A 计划(autorun
)自动运行,探长会打印张三的贷款(debit
)、存贷比(divisor
)。
这里须要说起一下,关于会计师从新计算的时机,是在探长执行 shouldCompute 的时候,探长发现会计师值 陈旧 了,就让会计师从新计算:
看看这里,对计算值而言,isComputedValue()
(若是是计算值)返回 true,就会执行 obj.get()
方法,这个方法刚才刚讲过,会让会计师执行 重量型计算操做,更新本身的计算值。
因此,此次计算时机并不是等到探长执行任务时(真正用到该值)的时候才让其从新计算,和第一次 autorun
的时机不一致。
估计这是 MobX 考虑到会计师的值确定须要更新的(已经肯定要被探长 R1 用到),还有可能会被其余上级引用,既然早晚要更新的,那就尽量将更新前置,这样在总体上能下降成本。
更新完以后,在探长执行任务的时候,会计师汇报本身是最新的值了,就不用再从新计算一遍。
虽然懒,可是懒得有技巧。
至此,有关会计师的源码解读已经差很少,后续有想到的再补充。
本文为了方便说明,因此单独使用 mobx.computed
方法定义计算值,平时使用中更多则是直接应用在 对象中属性 上,使用 get 语法:
var bankUser = mobx.observable({ income: 3, debit: 2, get divisor() { return this.income / this.debit; } });
这仅仅是写法上不同,源码分析的思路是一致的。
问题:当咱们更改张三贷款数额 bankUser.debit = 4;
时,请从源码角度解答 MobX 的执行流程是如何的?
参考答案提示:
reportChanged() => propagateChanged() => propagateMaybeChanged() => runReaction() => track() => get() => computeValue() => bindDependencies()
问题:若是不存在 autorun
(即没有探长参与,仅有观察员和会计师),此时仅改变张三存款数值:
var bankUser = mobx.observable({ income: 3, debit: 2 }); var divisor = mobx.computed(() => { return bankUser.income / bankUser.debit; }); bankUser.income = 6; // 请问此时的执行状况是什么样的? console.log('张三的存贷比:', divisor)
请问会计师会从新计算数值么?此时这套系统的执行状况又会是怎么样的呢?
参考答案提示:会计师此时执行 轻量级计算模式。
此篇文章讲解 MobX 中 计算值 (computedValue) 的概念,类比故事中的会计师角色。总结一下 计算值 (computedValue)的特征:
autorun
(或reaction
) 很像,之因此类似是在 执行任务 时都涉及到调用 trackDerivedFunction
方法;而对 autorun
(或reaction
)而言,计算值和观察值很相,都是数据提供者。正如 官方文档 而言,计算值是高度优化过的,因此尽量应用他们。
下一篇文章将探讨 MobX 中与 autorun
和 computed
相关的计算性能优化的机制,看看 MobX 如何平衡复杂场景下状态管理时的效率和性能。
下面的是个人公众号二维码图片,欢迎关注,及时获取最新技术文章。