-
初衷:以系列故事的方式展现源码逻辑,尽可能以易懂的方式讲解 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、最简单的情况
1var bankUser = mobx.observable({ 2 income: 3, 3 debit: 2 4}); 5 6mobx.autorun(() => { 7 console.log('张三的存贷:', income); 8}); 910bankUser.income = 4;
这里我们创建了 autorun
实例 (探长 R1)、observable
实例(观察员O1)
这个示例和我们之前在首篇文章《【用故事解读 MobX源码(一)】 autorun》中所用示例是一致的。
当执行 bankUser.income = 4;
语句的时候,观察员 O1 观察到的数值变化直接上报给探长 R1,然后探长就执行任务了。关系简单:
从代码层面上来讲,该 响应链 上的关键函数执行顺序如下:
1(O1) reportChange 2 -> (O1) propagateChanged 3 -> (R1) onBecomeStale 4 -> (R1) trackDerivedFunction 5 -> 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.3、有两个会计师的情况
我们继续在上一个示例上修改,再新增一个计算值 indication
(这个变量的创建没有特殊的含义,纯粹是为了做演示),由会计师 C2 了负责其进行计算。
1var bankUser = mobx.observable({ 2 income: 3, 3 debit: 2 4}); 5 6var divisor = mobx.computed(() => { 7 return bankUser.income / bankUser.debit; 8}); 910var indication = mobx.computed(() => {11 return divisor / (bankUser.income + 1);12});1314mobx.autorun(() => {15 console.log('张三的 indication', indication);16});1718bankUser.debit = 4;
大体成员和之前的示例相差不大,只是这次我们修改 bankUser.debit
变量(前面两个示例都是修改 bankUser.income
)。
这么做的目的是为了营造出下述的 响应链 结构,我们通过修改 bankUser.debit
变量,从而影响 会计师 C1,继而影响 会计师 C2,最终让探长 R1 执行任务。
同样的,我们从代码层面上来列出该 响应链 上的关键函数执行顺序,比上两个示例都复杂些,大致如下:
1(O2) reportChange 2 -> (O2) propagateChanged 3 -> (C1) propagateMaybeChanged 4 -> (C2) propagateMaybeChanged 5 -> (R1) onBecomeStale(这里并不会让探长 `runReaction`) 6-> (O2) endBatch 7 -> (R1) runReaction(到这里才让探长执行 `runReaction`) 8 -> (R1) shouldCompute 9 -> (C2) shouldCompute10 -> (C1) shouldCompute11 -> (C1) trackAndCompute12 -> (C1) propagateChangeConfirmed13 -> (C2) trackAndCompute14 -> (C2) propagateChangeConfirmed15 -> trackDerivedFunction16 -> 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
方法过程,一图胜千言: