================前言===================
- 初衷:以系列故事的方式展现 MobX 源码逻辑,尽可能以易懂的方式讲解源码;
-
本系列文章:
- 《【用故事解读 MobX源码(一)】 autorun》
- 《【用故事解读 MobX源码(二)】 computed》
- 《【用故事解读 MobX源码(三)】 shouldCompute》
- 《【用故事解读 MobX 源码(四)】装饰器 和 Enhancer》
- 《【用故事解读 MobX 源码(五)】 Observable》
- 文章编排:每篇文章分成两大段,第一大段以简单的侦探系列故事的形式讲解(所涉及人物、场景都以 MobX 中的概念为原型创建),第二大段则是源码讲解。
- 本文基于 MobX 4 源码讲解
=======================================
A. Story Time
宁静的早上,执行官 MobX 将自己的计算性能优化机制报告呈现给警署最高长官。
在这份报告解说中,谈及部署成本最高的地方是在执行任务部分。因此优化这部分任务执行机制,也就相当于优化性能。
警署最高长官浏览了报告前言部分,大致总结以下 2 点核心思想:
- 有两组人会涉及到任务的执行:执行组(探长) 和 计算组(会计师)
言外之意, 观察组(观察员)不在优化机制里,他们的行为仍旧按部就班,该汇报的时候就汇报,该提供数据的时候提供数据。
- 由于执行任务的比较消耗资源,因此执行人员对每一次任务的执行都要问一个”为什么“,最核心的一点是:如果下级人员的数据不是最新的时候,上级人员就不应该执行任务。
那么,执行人员依据什么样的规则来决定是否执行呢?
警署最高长官继续往下阅读,找到了解答该问题的详细解说。简言之,为了解决该问题执行官 MobX 给出了状态调整策略,并在这套策略之上指定的任务执行规则。
由于专业性较强,行文解释里多处使用代码。为了更生动形象地解释这套行为规范,执行官 MobX 在报告里采用 示例 + 图示 的方式给出生动形象的解释。
接下来我们在 B. Source Code Time 部分详细阐述这份 任务执行规则 的内容。
B. Source Code Time
执行人员(探长和会计师)依据什么样的规则来决定是否执行呢?
答案是,执行官 MobX 提供了一个名为 shouldCompute 的方法,每次执行人员(探长和会计师)需要执行之前都要调用该方法 —— 只有该方法返回 true
的时候才会执行任务(或计算)。
在源码里搜索一下关键字shouldCompute
,就可以知道的确只有 derivation(执行组,探长也属于执行组)、 reaction(探长)、 computeValue(会计师)这些有执行权力的人才能调用这个方法,而 observerable(观察员)并不在其中。
也就说 shouldCompute 就是任务执行规则,任务执行规则就是 shouldCompute。而背后支撑 shouldCompute 的则是一套 状态调整策略
1、状态调整策略
1.1、L 属性 和 D 属性
翻开 shouldCompute
源码, 将会看到 dependenciesState
属性。
其实这个 dependenciesState
(以下简称 D 属性) 属性还存在一个”孪生“属性lowestObserverState
(以下简称 L 属性)。这两个属性正是执行官 MobX 状态调整策略的核心。
L 属性 和 D 属性反映当前对象所处的状态, 都是枚举值,且取值区间都是一致的,只能是以下 4 个值之一:
- -1: 即 NOT_TRACKING,表示不在调整环节内(还未进入调整调整,或者已经退出调整环节)
- 0:即 UP_TO_DATE,表示状态很稳定
- 1: 即 POSSIBLY_STALE,表示状态有可能不稳定
- 2:即 STALE,表示状态不稳定
上面的文字表述比较枯燥,我们来张图感受一下:
我们以 “阶梯” 来表示上述的状态值;
- UP_TO_DATE(0) 是地面(表示“非常稳定”)
- POSSIBLY_STALE(1) 是第一个台阶
- STALE(2) 是第 2 个台阶,
- NOT_TRACKING(-1)则到地下一层去了
- 所谓 “高处不胜寒”,距离地面越高,就代表越不稳定。
- 状态值 UP_TO_DATE(0)代表的含义是 稳定的状态,是每个对象所倾向的状态值。
1.2、调整策略
依托L 属性 和 D 属性,执行官 MobX 的调整策略应运而生:
- 只有在 观察值发生变化 的时候(比如修改了
bankUser.income
属性值),才会启用这套机制; -
下级成员拥有 L 属性;而上级成员拥有 D 属性,比如:
- 观察员 O1 只拥有 L 属性
- 探长 R1 只拥有 D 属性
- 会计师 C1 既拥有 L 属性,也拥有 D 属性
- 某下级成员调整属性时,调整的策略必须要满足:自身的 D 属性 永远不大于(≤)上级的 L 属性
- 某上级成员调整属性时,调整的策略必须要满足:其下级成员的 D 属性 永远不大于(≤)自身的 L 属性
- 观察值的变更会让成员的属性值 上升(提高不稳定性),MobX 执行任务会让成员属性值 降低(不稳定性降低);
上述调整策略给我们的直观感受,就是外界的影响导致 MobX 执行官的部署系统不稳定性上升,为了消除这些不稳定,MobX 会尽可能协调各方去执行任务,从而消除这些个不稳定性。
(举个不甚恰当的例子,参考人类的免疫机制,病毒感冒后体温上升就是典型的免疫机制激活的外在表现,抵御完病毒之后体温又回归正常)
2、执行任务规则
我们知道,只有上级成员(探长或者设计师)才有执行任务的权力;而一旦满足上面的调整策略,在任何时刻,执行官 MobX 直接查阅该上级成员的 D 属性 就能断定该上级成员(探长或者设计师)是否需要执行任务了,非常简单方便。
执行官 MobX 判断的依据都体现在 shouldCompute 方法中了。
本人窃认为这个shouldCompute
函数的名字太过于抽象,如果让我命名的话,我更倾向于使用shouldExecuteTask
这个单词。
依托L 属性 和 D 属性,执行任务规则(即 shouldCompute
)就出炉了:
- 如果属性值为 NOT_TRACKING(-1)或者 STALE(2),说明自己所依赖的下级数值陈旧了,是时候该重新执行任务(或重新计算)了;
- 如果属性值为 UP_TO_DATE(0),说明所依赖的下级的数值没有更改,是稳定的,不需要重新执行任务。
- 如果属性值为 POSSIBLY_STALE(1),说明所依赖的值(一定是计算值,只有计算值的参与才会出现这种状态)有可能变更,需要让下级先确认完后再做进一步判断。这种情况可能不太好理解,后文会详细说明。
执行任务规则看上去比较简单,但应用到执行官 MobX 自动化部署方案中情况就复杂了。下面将通过 3 个场景,从简单到复杂,一步一步来演示L 属性和D 属性 是如何巧妙地融合到已有的部署方案中,并以最小的成本实现性能优化的。
2.1、最简单的情况
var bankUser = mobx.observable({
income: 3,
debit: 2
});
mobx.autorun(() => {
console.log('张三的存贷:', income);
});
bankUser.income = 4;
这里我们创建了 autorun
实例 (探长 R1)、observable
实例(观察员O1)
这个示例和我们之前在首篇文章《【用故事解读 MobX源码(一)】 autorun》中所用示例是一致的。
当执行 bankUser.income = 4;
语句的时候,观察员 O1 观察到的数值变化直接上报给探长 R1,然后探长就执行任务了。关系简单:
从代码层面上来讲,该 响应链 上的关键函数执行顺序如下:
(O1) reportChange
-> (O1) propagateChanged
-> (R1) onBecomeStale
-> (R1) trackDerivedFunction
-> fn(即执行 autorun 中的回调)
其中涉及到 L、D属性 更改的函数有 propagateChanged
和 track
这两个。
Step 1:在 propagateChanged 方法执行时,让观察员 O1 的 L 属性 从 0 → 2 ,按照上述的调整原则,探长 R1 的 D属性 必须要高于观察员 O1 的 L 属性,所以其值也只能用从 0 → 2。
Step 2:而随着 trackDerivedFunction 方法的执行(即探长执行任务)后,观察员 O1 的 L 属性 又从 2 → 0,同时也让探长 R1 的 D属性 从 2 → 0;
在这里我们已经可以明显感受到 非稳态的上升 和 削减 这两个阶段:
- 非稳态的上升:外界更改
bankUser.income
属性,触发propagateChanged
方法,从而让观察员的 L 属性 以及探长的 D属性 都变成了 2 ,这是系统趋向不稳定的表现。从 层级上来看,是自下而上的过程。 - 非稳态的削减:随着变更的传递,将触发探长 R1 的
onBecameStale
方法。执行期间 MobX 执行官查阅探长的 D属性 是 2,依据shouldCompute
中的执行规定,同意让探长执行任务。执行完之后,观察员的 L 属性、探长的 D属性 都下降为 0,表示系统又重新回到稳定状态。从 层级上来看,是自上而下的过程。
2.2、有单个会计师的情况
上面介绍了最简单的情况,只有一个探长 R1(autorun
)和一个观察员 O1(income
)。
现在我们将环境稍微弄复杂一些,新增一个 会计师 C1(divisor
) ,此时再来看看上述的变更原则是如何在系统运转时起作用的:
var bankUser = mobx.observable({
income: 3,
debit: 2
});
var divisor = mobx.computed(() => {
return bankUser.income / bankUser.debit;
});
mobx.autorun(() => {
console.log('张三的 divisor:', divisor);
});
bankUser.income = 4;
这个示例和我们之前在首篇文章《【用故事解读 MobX源码(二)】 computed 》中所用示例是一致的。
当我们执行 bankUser.income = 4;
语句的时候,观察员 O1 先上报给会计师 C1,接着会计师 C1 会重新执行计算任务后,上报给探长,探长R1 再重新执行任务。
上面描述起来比较简单,但从代码层面上来讲还是有些绕,先列出该 响应链 上的关键函数执行顺序如下(很明显比上面的示例要稍微复杂一些):
(O1) reportChange
-> (O1) propagateChanged
-> (C1) propagateMaybeChanged
-> (R1) onBecomeStale(这里并不会让探长 `runReaction`)
-> (O1) endBatch
-> (R1) runReaction(到这里才让探长执行 `runReaction`)
-> (C1) reportObserved
-> (C1) shouldCompute
-> (C1) trackAndCompute
-> (C1) propagateChangeConfirmed
-> (R1) trackDerivedFunction
-> fn(即执行 autorun 中的回调)
注:这里还需要啰嗦一句,虽然这里会触发探长 R1 的
onBecomeStale
方法,但 MobX 并不会直接让探长执行任务,这也是 MobX 优化的一种手段体现,详细分析请移步《 【用故事解读 MobX源码(二)】 computed 》。
Step 1:在 propagateChanged 方法执行时,让观察员 O1 的 L 属性 从 -1 → 2 ,按照上述的调整原则,其直接上级 C1 的 D属性 必须要高于观察员 O1 的 L 属性,所以其值也只能用从 0 → 2;
和上述简单示例中最大的不同,在于该期间还涉及到会计师 C1 的状态更改,具体表现就是调用 propagateMaybeChanged ,在该方法执行后让会计师 C1 的 L 属性 从 0 → 1 ,其直接上级 R1 的 D属性 必须要高于会计师 C1 的 L 属性,所以其值也从 0 → 1;
注:虽然观察员 O1 的状态更改 不能直接 触发探长 R1 的状态更改,却可以凭借会计师 C1 间接 地让 探长 R1 的状态发生更改。
Step 2:此步骤是以 会计师 状态变更为中心演变过程,上一个案例并不存在会计师,所以并不会有该步骤。通过 trackAndCompute 方法,会计师 C1 的 D 属性 又从 2 → 0,同时也让观察员 O1 的 L属性 从 2 → 0;这个过程表明会计师 C1 的计算值已经更新了。
随后在 propagateChangeConfirmed 中让探长 R1 的 D 属性 从 1 (下级数值可能有更新)→ 2 (确定下级数值确定有更新),同时也让会计师 C1 的 L 属性 从 1(告知上级自己的值可能有更新)→ 2 (告知上级自己的值的确有更新);表明探长 R1 和 会计师 C1 的稳态还未达成,需要 Step 3 的执行去消除非稳态。
Step 3:会计师的计算值 C1 更新完毕之后,探长才执行任务。通过 trackDerivedFunction 方法的执行(即探长执行任务)后,会计师 C1 的 L 属性 又从 2 → 0,同时也让探长 R1 的 D 属性 从 2 → 0;
虽然这个示例中,状态的变更比上面的示例要复杂一些,不过我们依然可以从整体上感受到 非稳态的上升 和 削减 这两个阶段:
- 非稳态的上升:外界更改
bankUser.income
属性,触发propagateChanged
方法,从而让观察员 O1 的 L 属性 以及会计师 C1 的 D属性 都变成了 2 ,同时让会计师 C1 的 L 属性 以及探长 R1 的 D属性 都变成了 1 。这是系统趋向不稳定的表现。从 层级上来看,是自下而上的过程。 - 非稳态的削减:随着变更的传递,有两次削减非稳态的手段: ① 让会计师 C1 重新计算; ② 让探长执行任务。这两个阶段结束之后,所有成员的属性都下降为 0,表示系统又重新回到稳定状态。从 层级上来看,是自上而下的过程。
2.3、有两个会计师的情况
我们继续在上一个示例上修改,再新增一个计算值 indication
(这个变量的创建没有特殊的含义,纯粹是为了做演示),由会计师 C2 了负责其进行计算。
var bankUser = mobx.observable({
income: 3,
debit: 2
});
var divisor = mobx.computed(() => {
return bankUser.income / bankUser.debit;
});
var indication = mobx.computed(() => {
return divisor / (bankUser.income + 1);
});
mobx.autorun(() => {
console.log('张三的 indication', indication);
});
bankUser.debit = 4;
大体成员和之前的示例相差不大,只是这次我们修改 bankUser.debit
变量(前面两个示例都是修改 bankUser.income
)。
这么做的目的是为了营造出下述的 响应链 结构,我们通过修改 bankUser.debit
变量,从而影响 会计师 C1,继而影响 会计师 C2,最终让探长 R1 执行任务。
同样的,我们从代码层面上来列出该 响应链 上的关键函数执行顺序,比上两个示例都复杂些,大致如下:
(O2) reportChange
-> (O2) propagateChanged
-> (C1) propagateMaybeChanged
-> (C2) propagateMaybeChanged
-> (R1) onBecomeStale(这里并不会让探长 `runReaction`)
-> (O2) endBatch
-> (R1) runReaction(到这里才让探长执行 `runReaction`)
-> (R1) shouldCompute
-> (C2) shouldCompute
-> (C1) shouldCompute
-> (C1) trackAndCompute
-> (C1) propagateChangeConfirmed
-> (C2) trackAndCompute
-> (C2) propagateChangeConfirmed
-> trackDerivedFunction
-> fn(即执行 autorun 中的回调)
Step 1:在 propagateChanged 方法执行时,让观察员 O1 的 L 属性 从 0 → 2 ,按照上述的调整原则,其直接上级 C1 的 D属性 必须要高于观察员 O1 的 L 属性,所以其值也只能用从 0 → 2;
该期间还涉及到会计师 C1、C2 的状态更改,具体表现就是调用 propagateMaybeChanged ,在该方法执行后让会计师 C1、C2 的 L 属性 从 0 → 1 ,他们各自的直接上级 C2、 R1 的 D属性 值也从 0 → 1;
描述起来比较复杂,其实无非就是多了一个 会计师 C2 的 propagateMaybeChanged
方法过程,一图胜千言:
Step 2:此步骤是以 会计师 状态变更为中心演变过程,该步骤是上一个示例中 Step 2 的“复数”版,多个人参与就复杂些,不过条理还是清晰明了的。上个示例中只有一个会计师,所以 trackAndCompute ->propagateChangeConfirmed 的过程只有一次,而这里有两个会计师,所以这个过程就有两次(下图中两个蓝框);
经过该步骤之后会计师 O2、C1 的 L 属性 又从 2 → 0,同时也让C1、C2 的 D 属性 从 2 → 0;这个过程表明观察员 O1 和 会计师 C1 的计算值已经更新,达到稳态。
而 C2 的 L 属性 、探长 R1 的 D 属性 又从 0 → 2,表明探长 R1 和 会计师 C2 的稳态还未达成,需要 Step 3 的执行去消除非稳态。
Step 3:探长执行任务,通过 trackDerivedFunction 方法的执行(即探长执行任务)后,会计师 C2 的 L 属性 又从 2 → 0,同时也让探长 R1 的 D 属性 从 2 → 0;这一步和上个示例中的 Step 3 几乎相同。
在这个示例中,状态的变更纵使比上面的示例要复杂得多,但我们还是很清晰地从整体上感受到 非稳态的上升 和 削减 这两个阶段:
- 非稳态的上升:外界更改
bankUser.debit
属性,触发propagateChanged
方法,从而让观察员 O1 开始,依次影响 会计师 C1、C2,以及探长 R1 的 L、D 属性从 0 变成 1 或者 2,这是系统趋向不稳定的表现。从 层级上来看,是自下而上的过程。 - 非稳态的削减:随着变更的传递,有两次削减非稳态的手段: ① 让会计师 C1 、C2 重新计算; ② 让探长 R1 执行任务。这两个阶段结束之后,所有成员的属性都下降为 0,表示系统又重新回到稳定状态。从 层级上来看,是自上而下的过程。
2.4、一点点总结
通过上面三个从简单逐步到复杂的示例,我们简单总结归纳一下 MobX 在处理状态变更过程中所采取执行机制以及其背后的调整策略:
- 先是自下而上传递非稳态:这是一个自下而上的过程,由观察员发起这个过程,在这个过程中依次将外界的变更层层向上传递,改变每个相关成员的 L、D属性。 这个期间会拒绝一切成员任务执行的申请(比如探长执行任务、会计师执行计算任务等等)。
- 其次自上而下消解非稳态:这是一个自上而下的过程。当非稳态到达顶层后,由顶层人员(一般是探长类)开始做决策执行任务,在执行任务中凡是遇到有非稳态的成员(比如会计师、观察员),责令他们更新状态,消除非稳态,逐层逐层地消除非稳态。等整个任务执行完之后,每个成员都处于稳态状态,开始下一个变更的到来。
3、状态图
在软件设计中,为了更好地显示这种状态变更和事件之间的关系,常常使用 状态图 来展现(没错,就是 UML建模中的那个状态图)
如果不太熟悉,这里给个参考文章 UML建模之状态图(Statechart Diagram) 方便查阅。
挨个总结上述 3 个案例中 L、D属性,我们将其中的事件和属性改变抽离出来,就能获取状态图了,方便我们从另外一个角度理解和体会。
3.1、L 属性
Observable(观察员)、ComputeValue(会计师)这两种类型拥有 L 属性 :
3.2、D 属性
Reaction(探长)、ComputeValue(会计师)这两种类型拥有 D 属性:
所以,会计师同时拥有 L属性 和 D 属性
4、小测试
如果我们将 2.3、有两个会计师的情况 示例中的 bankUser.debit = 4;
修改成 bankUser.income = 6;
的话,那各个成员对象的 D 属性、L 属性 的变化情况又是怎么样的?
5、本文总结
如何在复杂的场景下兼顾计算性能?
MobX 提供了 shouldCompute
方法用于直接判断是否执行计算(或任务),判断的依据非常简单,只要根据对象的 dependenciesState
属性是否为 true
就能直接作出判断。
而其背后的支持则是 dependenciesState
属性(上文中的 D 属性)和 lowestObserverState
(上文中的 L 属性),这两个属性依托 MobX 中自动化机制在适当时机(搭”顺风车“)进行变更。因此,无论多么复杂的场景下 MobX 能以低廉的成本兼顾性能方面的治理,充分运用惰性求值思想减少计算开销。
初看 MobX 源码,它往往给你一种 ”杂项丛生“的感觉(调试这段代码的时候真是心里苦啊),但其实在这背后运转着一套清晰的 非稳态传递 和 非稳态削减 的固定模式,一旦掌握这套模式之后,MobX 自动化响应体系的脉络已清晰可见,这将为你更好理解 MobX 的运行机制打下扎实的基础。
到本篇为止,我们已经耗费 3 篇文章来解释 MobX 的(绝大部分)自动化响应机制。经过这 3 篇文章,读者应该对 MobX 的整个运转机制有了一个比较清晰明了的理解。后续的文章中将逐渐缩减”故事“成分,将讲解重心转移到 MobX 本身概念(比如 Observable
、decorator
、Atom
等)源码的解读上,相信有了这三篇文章的作为打底,理解其余部分更多的是在语法层面,阅读起来将更加游刃有余。
下面的是我的公众号二维码图片,欢迎关注,及时获取最新技术文章。