MobX底层原理与透明响应式实现
单次执行
同步推导
计算值与响应
MobX基础设计理念阐述
数周之前 Bertalan Miklos 撰写了一篇非常精彩的对比MobX与 基于Proxy的NX系列框架的博文。该文不仅详细解释了基于Proxy的响应式框架的原理,还介绍了MobX以及背后的Transparent Reactivity (Automatic Reactivity)的理念,这其中有很多我本人之前都尚未详细阐述的。本文即是我分享关于MobX的独特特性的文章。
Derivations Synchronously | MobX为何进行同步推导
MobX的重要特性之一即是所有的Derivations都是同步计算的,这一点很不同于现有的主流框架。对于RxJS这样的Event Stream流派的响应式框架,虽然它们也能保证同步推导,但是缺乏了所谓的透明追踪(Transparent Tracking)的特性。而对于Meteor、Knockout、Angular、Ember以及Vue这些主流的双向数据绑定的MVVM框架的响应式表现很类似于MobX,但是它们在可预测性(Predictability)上的表现差强人意。形象的解释就是如果某个框架重复运行你的代码(譬如重复渲染)或者延迟执行时,开发者很难进行调试,即使是像Promise
这样的简单的抽象都因为其异步性而难以调试。Flux或者Redux这样的Action-Dispatcher框架的核心优势即是在项目动态扩展的同时保证了其可预测性。MobX则另辟蹊径,没有像Redux这样基于纯函数的运行与数据流追踪,而是尝试从根本上去解决这个问题。Transparent Reactivity本身具有声明式、高阶以及简洁明了的特性,而MobX在此之上添加了两个约束:
- 对于任何状态的更改,MobX会保证所有相关的Derivation只会被执行一次。
- Derivations没有任何的延迟,对于Observer而言完全是同步进行的。
约束一着眼于解决多次运行的问题(Double Runs),即如果某个推导值依赖于其他推导值,整个Derivations会以正确的顺序执行。因此任何的推导值都不会有所谓的过时的、不一致状态出现,关于其实现原理参考之前的博文。约束二:所有的Derivations都不会是旧值 则更有意思了。约束二主要是为了解决所谓的临时不一致(Temporary Inconsistencies)的情形,这也是MobX采用了与其他UI库不一样的更新调度(Scheduling Derivation)策略。目前的UI库采取的策略往往是脏检测机制,即首先将发生变化的Derivations标记为脏数据,然后在下一轮更新的时候重新运行相关渲染。这种方式简单粗暴,适用于专注DOM更新的场景。DOM本身就具有一定的延迟,我们往往也不会选择在程序里从DOM中读取数据,因此临时的数据不一致性(Temporary Staleness)是可以被容忍的。而这种临时的数据不一致性却是任何响应式库的致命缺陷,譬如下面这个例子:
const user = observable({
firstName: “Michel”,
lastName: “Weststrate”,
fullName: computed(function() {
return this.firstName + " " + this.lastName
})
})
user.lastName = “Vaillant”
sendLetterToUser(user)
问题来了,在上述代码中当我们调用sendLetterToUser(user)
这一句时,我们能够确定读取到的user
对象中的fullName
值是更新之后的值还是旧值?在MobX中我们能够确定整个推导过程是同步进行的,即永远得到的都是最新的正确的值。这一特性能够保证程序的可预测性,并且MobX在调用栈中记录了完整的更新过程,使得调试也变得简单很多。当你想要了解某个值是如何计算而来的,只需要查看整个更新调用栈即可。在MobX项目最初启动的时候,很多人都会质疑我们是否能够保证高效地同时达到整个Derivation Tree的按序计算与每次更新都会触发属性值的重推导。
Transactions & Actions
了解React setState原理的同学肯定都对其中的事务(Transaction)的概念不陌生,我们应该将多个更新包裹在某个事务内然后一次性计算以提高性能。MobX中我们同样存在事务的概念,实际上所有的Derivations都是在某个事务的结束期进行计算的,不过如果用户在事务结束前就去读取某个属性,那么MobX也会保证得到正确的、最新的推导值。MobX 3中将事务相关的接口修正为了内部接口,而 actions 本身是会自动包裹在事务内的。Actions即用于声明某个函数会更新状态,其与reactions相辅相成,后者代表某个函数会响应状态的变化。
Computed Values & Reactions