【用故事解读 MobX源码(三)】 shouldCompute

================前言===================html

=======================================git

A. Story Time

宁静的早上,执行官 MobX 将本身的计算性能优化机制报告呈现给警署最高长官。github

在这份报告解说中,谈及部署成本最高的地方是在执行任务部分。所以优化这部分任务执行机制,也就至关于优化性能。segmentfault

警署最高长官浏览了报告前言部分,大体总结如下 2 点核心思想:性能优化

  • 两组人会涉及到任务的执行:执行组(探长) 和 计算组(会计师)
言外之意, 观察组(观察员)不在优化机制里,他们的行为仍旧循序渐进,该汇报的时候就汇报,该提供数据的时候提供数据。
  • 因为执行任务的比较消耗资源,所以执行人员对每一次任务的执行都要问一个”为何“,最核心的一点是:若是下级人员的数据不是最新的时候,上级人员就不该该执行任务。

clipboard.png

那么,执行人员依据什么样的规则来决定是否执行呢?微信

警署最高长官继续往下阅读,找到了解答该问题的详细解说。简言之,为了解决该问题执行官 MobX 给出了状态调整策略,并在这套策略之上指定的任务执行规则函数

因为专业性较强,行文解释里多处使用代码。为了更生动形象地解释这套行为规范,执行官 MobX 在报告里采用 示例 + 图示 的方式给出生动形象的解释。性能

接下来咱们在 B. Source Code Time 部分详细阐述这份 任务执行规则 的内容。测试

B. Source Code Time

执行人员(探长和会计师)依据什么样的规则来决定是否执行呢?

答案是,执行官 MobX 提供了一个名为 shouldCompute 的方法,每次执行人员(探长和会计师)须要执行以前都要调用该方法 —— 只有该方法返回 true 的时候才会执行任务(或计算)。

在源码里搜索一下关键字 shouldCompute,就能够知道的确只有 derivation(执行组,探长也属于执行组)、 reaction(探长)、 computeValue(会计师)这些有执行权力的人才能调用这个方法,而 observerable(观察员)并不在其中。
clipboard.png

也就说 shouldCompute 就是任务执行规则任务执行规则就是 shouldCompute。而背后支撑 shouldCompute 的则是一套 状态调整策略

一、状态调整策略

1.一、L 属性D 属性

翻开 shouldCompute 源码, 将会看到 dependenciesState 属性。

clipboard.png

其实这个 dependenciesState(如下简称 D 属性) 属性还存在一个”孪生“属性lowestObserverState (如下简称 L 属性)。这两个属性正是执行官 MobX 状态调整策略的核心。

L 属性D 属性反映当前对象所处的状态, 都是枚举值,且取值区间都是一致的,只能是如下 4 个值之一:

  • -1: 即 NOT_TRACKING,表示不在调整环节内(还未进入调整调整,或者已经退出调整环节)
  • 0:即 UP_TO_DATE,表示状态很稳定
  • 1: 即 POSSIBLY_STALE,表示状态有可能不稳定
  • 2:即 STALE,表示状态不稳定

上面的文字表述比较枯燥,咱们来张图感觉一下:

clipboard.png

咱们以 “阶梯” 来表示上述的状态值;

  • UP_TO_DATE(0) 是地面(表示“很是稳定”)
  • POSSIBLY_STALE(1) 是第一个台阶
  • STALE(2) 是第 2 个台阶,
  • NOT_TRACKING(-1)则到地下一层去了
  • 所谓 “高处不胜寒”,距离地面越高,就表明越不稳定
  • 状态值 UP_TO_DATE(0)表明的含义是 稳定的状态,是每一个对象所倾向的状态值。

1.二、调整策略

依托L 属性D 属性,执行官 MobX 的调整策略应运而生:

  • 只有在 观察值发生变化 的时候(好比修改了 bankUser.income 属性值),才会启用这套机制;
  • 下级成员拥有 L 属性;而上级成员拥有 D 属性,好比:

    • 观察员 O1 只拥有 L 属性
    • 探长 R1 只拥有 D 属性
    • 会计师 C1 既拥有 L 属性,也拥有 D 属性
  • 某下级成员调整属性时,调整的策略必需要知足:自身的 D 属性 永远不大于(≤)上级的 L 属性
  • 某上级成员调整属性时,调整的策略必需要知足:其下级成员的 D 属性 永远不大于(≤)自身的 L 属性
  • 观察值的变动会让成员的属性值 上升(提升不稳定性),MobX 执行任务会让成员属性值 下降(不稳定性下降);

上述调整策略给咱们的直观感觉,就是外界的影响致使 MobX 执行官的部署系统不稳定性上升,为了消除这些不稳定,MobX 会尽量协调各方去执行任务,从而消除这些个不稳定性
(举个不甚恰当的例子,参考人类的免疫机制,病毒感冒后体温上升就是典型的免疫机制激活的外在表现,抵御完病毒以后体温又回归正常)

二、执行任务规则

咱们知道,只有上级成员(探长或者设计师)才有执行任务的权力;而一旦知足上面的调整策略,在任什么时候刻,执行官 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.一、最简单的状况

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,而后探长就执行任务了。关系简单:

upstream

从代码层面上来说,该 响应链 上的关键函数执行顺序以下:

(O1) reportChange 
    -> (O1) propagateChanged 
    -> (R1) onBecomeStale 
      -> (R1) trackDerivedFunction 
         -> fn(即执行 autorun 中的回调)

其中涉及到 L、D属性 更改的函数有 propagateChangedtrack 这两个。

Step 1:在 propagateChanged 方法执行时,让观察员 O1 的 L 属性 从 0 → 2 ,按照上述的调整原则,探长 R1 的 D属性 必需要高于观察员 O1 的 L 属性,因此其值也只能用从 0 → 2。

pagechagned

Step 2:而随着 trackDerivedFunction 方法的执行(即探长执行任务)后,观察员 O1 的 L 属性 又从 2 → 0,同时也让探长 R1 的 D属性 从 2 → 0;

track

在这里咱们已经能够明显感觉到 非稳态的上升削减 这两个阶段:

  • 非稳态的上升:外界更改 bankUser.income 属性,触发 propagateChanged 方法,从而让观察员的 L 属性 以及探长的 D属性 都变成了 2 ,这是系统趋向不稳定的表现。从 层级上来看,是自下而上的过程。
  • 非稳态的削减:随着变动的传递,将触发探长 R1 的 onBecameStale 方法。执行期间 MobX 执行官查阅探长的 D属性 是 2,依据 shouldCompute 中的执行规定,赞成让探长执行任务。执行完以后,观察员的 L 属性、探长的 D属性 都降低为 0,表示系统又从新回到稳定状态。从 层级上来看,是自上而下的过程。

2.二、有单个会计师的状况

上面介绍了最简单的状况,只有一个探长 R1(autorun)和一个观察员 O1(income)。

如今咱们将环境稍微弄复杂一些,新增一个 会计师 C1divisor) ,此时再来看看上述的变动原则是如何在系统运转时起做用的:

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 再从新执行任务。

c1 upstream

上面描述起来比较简单,但从代码层面上来说仍是有些绕,先列出该 响应链 上的关键函数执行顺序以下(很明显比上面的示例要稍微复杂一些):

(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;

maybechanged

注:虽然观察员 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 的执行去消除非稳态。

trackAndCompute

Step 3:会计师的计算值 C1 更新完毕以后,探长才执行任务。经过 trackDerivedFunction 方法的执行(即探长执行任务)后,会计师 C1 的 L 属性 又从 2 → 0,同时也让探长 R1 的 D 属性 从 2 → 0;

track

虽然这个示例中,状态的变动比上面的示例要复杂一些,不过咱们依然能够从总体上感觉到 非稳态的上升削减 这两个阶段:

  • 非稳态的上升:外界更改 bankUser.income 属性,触发 propagateChanged 方法,从而让观察员 O1 的 L 属性 以及会计师 C1 的 D属性 都变成了 2 ,同时让会计师 C1 的 L 属性 以及探长 R1 的 D属性 都变成了 1 。这是系统趋向不稳定的表现。从 层级上来看,是自下而上的过程。
  • 非稳态的削减:随着变动的传递,有两次削减非稳态的手段: ① 让会计师 C1 从新计算; ② 让探长执行任务。这两个阶段结束以后,全部成员的属性都降低为 0,表示系统又从新回到稳定状态。从 层级上来看,是自上而下的过程。

2.三、有两个会计师的状况

咱们继续在上一个示例上修改,再新增一个计算值 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 执行任务。

two compute

一样的,咱们从代码层面上来列出该 响应链 上的关键函数执行顺序,比上两个示例都复杂些,大体以下:

(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;

该期间还涉及到会计师 C一、C2 的状态更改,具体表现就是调用 propagateMaybeChanged ,在该方法执行后让会计师 C一、C2 的 L 属性 从 0 → 1 ,他们各自的直接上级 C二、 R1 的 D属性 值也从 0 → 1;

描述起来比较复杂,其实无非就是多了一个 会计师 C2 的 propagateMaybeChanged 方法过程,一图胜千言:

c2 upstream

Step 2:此步骤是以 会计师 状态变动为中心演变过程,该步骤是上一个示例中 Step 2 的“复数”版,多我的参与就复杂些,不过条理仍是清晰明了的。上个示例中只有一个会计师,因此 trackAndCompute ->propagateChangeConfirmed 的过程只有一次,而这里有两个会计师,因此这个过程就有两次(下图中两个蓝框);

c2 compute

通过该步骤以后会计师 O二、C1 的 L 属性 又从 2 → 0,同时也让C一、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 几乎相同。

c2 track

在这个示例中,状态的变动纵使比上面的示例要复杂得多,但咱们仍是很清晰地从总体上感觉到 非稳态的上升削减 这两个阶段:

  • 非稳态的上升:外界更改 bankUser.debit 属性,触发 propagateChanged 方法,从而让观察员 O1 开始,依次影响 会计师 C一、C2,以及探长 R1 的 L、D 属性从 0 变成 1 或者 2,这是系统趋向不稳定的表现。从 层级上来看,是自下而上的过程。
  • 非稳态的削减:随着变动的传递,有两次削减非稳态的手段: ① 让会计师 C1 、C2 从新计算; ② 让探长 R1 执行任务。这两个阶段结束以后,全部成员的属性都降低为 0,表示系统又从新回到稳定状态。从 层级上来看,是自上而下的过程。

2.四、一点点总结

经过上面三个从简单逐步到复杂的示例,咱们简单总结概括一下 MobX 在处理状态变动过程当中所采起执行机制以及其背后的调整策略:

  • 先是自下而上传递非稳态:这是一个自下而上的过程,由观察员发起这个过程,在这个过程当中依次将外界的变动层层向上传递,改变每一个相关成员的 L、D属性。 这个期间会拒绝一切成员任务执行的申请(好比探长执行任务、会计师执行计算任务等等)。
  • 其次自上而下消解非稳态:这是一个自上而下的过程。当非稳态到达顶层后,由顶层人员(通常是探长类)开始作决策执行任务,在执行任务中凡是遇到有非稳态的成员(好比会计师、观察员),责令他们更新状态,消除非稳态,逐层逐层地消除非稳态。等整个任务执行完以后,每一个成员都处于稳态状态,开始下一个变动的到来。

三、状态图

在软件设计中,为了更好地显示这种状态变动和事件之间的关系,经常使用 状态图 来展示(没错,就是 UML建模中的那个状态图)

若是不太熟悉,这里给个参考文章 UML建模之状态图(Statechart Diagram) 方便查阅。

挨个总结上述 3 个案例中 L、D属性,咱们将其中的事件和属性改变抽离出来,就能获取状态图了,方便咱们从另一个角度理解和体会。

3.一、L 属性

Observable(观察员)、ComputeValue(会计师)这两种类型拥有 L 属性

L attr

3.二、D 属性

Reaction(探长)、ComputeValue(会计师)这两种类型拥有 D 属性
D attr

因此,会计师同时拥有 L属性D 属性

四、小测试

若是咱们将 2.三、有两个会计师的状况 示例中的 bankUser.debit = 4; 修改为 bankUser.income = 6; 的话,那各个成员对象的 D 属性L 属性 的变化状况又是怎么样的?

五、本文总结

如何在复杂的场景下兼顾计算性能?

MobX 提供了 shouldCompute 方法用于直接判断是否执行计算(或任务),判断的依据很是简单,只要根据对象的 dependenciesState 属性是否为 true 就能直接做出判断。

而其背后的支持则是 dependenciesState 属性(上文中的 D 属性)和 lowestObserverState (上文中的 L 属性),这两个属性依托 MobX 中自动化机制在适当时机(搭”顺风车“)进行变动。所以,不管多么复杂的场景下 MobX 能以低廉的成本兼顾性能方面的治理,充分运用惰性求值思想减小计算开销

初看 MobX 源码,它每每给你一种 ”杂项丛生“的感受(调试这段代码的时候真是内心苦啊),但其实在这背后运转着一套清晰的 非稳态传递非稳态削减 的固定模式,一旦掌握这套模式以后,MobX 自动化响应体系的脉络已清晰可见,这将为你更好理解 MobX 的运行机制打下扎实的基础。

到本篇为止,咱们已经耗费 3 篇文章来解释 MobX 的(绝大部分)自动化响应机制。通过这 3 篇文章,读者应该对 MobX 的整个运起色制有了一个比较清晰明了的理解。后续的文章中将逐渐缩减”故事“成分,将讲解重心转移到 MobX 自己概念(好比 ObservabledecoratorAtom等)源码的解读上,相信有了这三篇文章的做为打底,理解其他部分更多的是在语法层面,阅读起来将更加游刃有余。

下面的是个人公众号二维码图片,欢迎关注,及时获取最新技术文章。
微信公众号

相关文章
相关标签/搜索