首先,先让咱们看一下Mobx的核心概念。react
可观察状态(Observable state)。 任何能够变异且能够做为计算值源的值都是state。Mobx能够开箱即用地使绝大多数类型的值(基本类型,数组,类,对象)变成可观察的(甚至深度观察)。web
计算值(Computed values)。 任何可观察的值可使用纯函数计算得出任何值。计算值的范围能够从简单的字符串到复杂的对象甚至dom操做上。计算值会懒惰地对状态变化作出反应。算法
反应(Reactions)。 反应有点相似于计算值,可是它不产生新值,而是做为桥梁衔接了响应式编程和命令式编程,产生一个反作用(I/O操做),例如打印到控制台,发送网络请求,更新dom树等。编程
操做(Actions)。 操做是改变状态(state)的主要手段。操做不是状态改变后的反应,是改变的来源,例如用户事件或web-socket链接,用以改变可观察的状态。后端
计算值和反应在本文章的后续中都称为衍生(derivations)。到目前为止,这听起来可能有点学术性,因此,咱们让它具体点。在excel电子表格中,全部具备值的数据单元格都是可观察状态的,公式和图标是能够从数据单元格和其余公式衍生的计算值。在屏幕上绘制数据单元格或公式的结果就是一个反应(reaction),改变数据单元格或公式就是一个操做(action)数组
下面这个例子结合了Mobx和React,而且包含了以上4个概念:缓存
class Person {
@observable firstName = "Michel";
@observable lastName = "Weststrate";
@observable nickName;
@computed get fullName() {
return this.firstName + " " + this.lastName;
}
}
const michel = new Person();
// Reaction: log the profile info whenever it changes
autorun(() => console.log(person.nickName ? person.nickName : person.fullName));
// Example React component that observes state
const profileView = observer(props => {
if (props.person.nickName)
return <div>{props.person.nickName}</div>
else
return <div>{props.person.fullName}</div>
});
// Action:
setTimeout(() => michel.nickName = "mweststrate", 5000)
React.render(React.createElement(profileView, { person: michel }), document.body);
// This snippet is runnable in jsfiddle: https://jsfiddle.net/mweststrate/049r6jox/
view rawprofile.jsx hosted with ❤ by GitHub
复制代码
咱们能够画一张依赖图:网络
这个应用中,状态由可观察状态捕获(蓝色图标)。绿色的fullName是计算值,由可观察状态firstName和lastName自动衍生得出。一样,profileView由nickName和fullName衍生得出。profileView将经过产生反作用来响应状态更改——它更新React组件树。闭包
当使用Mobx时,依赖关系树被最低限度定义。举个例子,一旦profileView的有了nickName,且渲染再也不受fullName的值的影响,也不受firstName和lastName的影响,那么他们之间全部的观察者关系将被清除,Mobx将自动的简化依赖树,以下图:并发
Mobx老是使用最小化计算次数去产生状态。接下来,我将介绍用于实现此目标的几种策略,但在深刻了解计算值和反应如何与状态同步以前,让咱们首先描述Mobx背后的原理:
对状态变化作出反应老是比对状态变化作出动做要好。(Reacting to state changes is always better then acting on state changes.)
应用程序响应状态更改的必要操做是一般会建立或更新一些值,大多数的操做(actions)管理着本地缓存。dom更新、批量更新值、请求后端,这些均可以被认为变相地使缓存失效。要确保这些缓存保持同步,你须要订阅(subscribe)将来的状态更改,以便再次触发你的操做。
可是观察者模式有一个基本的问题:当你的应用变大时,你可能会犯错,好比依然订阅再也不使用的值或忘记订阅一些值。
像flux风格的订阅很容易出现这种超额订阅的状况。使用React时,你能够经过在渲染中打印来判断你的组件是否被超额订阅了。Mobx会将打印出的超额订阅数减小到0。这个想法很简单但违反直觉:订阅越多,从新计算越少。Mobx为你管理数千个观察者,你能够有效地权衡内存的CPU周期。
超额订阅也以很是微妙的形式存在。若是你订阅了使用的数据,但并未在全部条件下订阅,那么你依然须要超额订阅。例如,若是profileView组件订阅了fullName,而且profileView有nickName,这就将超额订阅。所以Mobx设计背后的一个重要原则是:
在运行时才能实现最小、一致地订阅子集(A minimal, consistent set of subscriptions can only be achieved if subscriptions are determined at run-time)。
Mobx背后的第二个重要思想是,对于任何比TodoMVC更复杂的应用程序,一般须要一个数据图而不是规范化的树,以一种最佳方式存储状态。数据图能够实现参照一致性并避免数据重复,从而保证衍生值永远不会过期。
Mobx如何有效地将全部衍生保持在一个一致地状态?
答案是:不缓存,只有在须要时再从新计算衍生。这不是很昂贵吗?Mobx认为不是,反而这是很高效地。缘由是Mobx不会运行全部衍生,但确保参与reaction的computed values与可观察状态保持同步。这些衍生被称为响应式的。再次以excel举例:只有当那些被观察着的当前可见或被间接使用的公式发生变化时,才会去从新计算值。
Lazy versus reactive evaluation
那么反应没有直接或间接使用的计算呢?你依然能够随时检查计算值的值,如fullName。答案是简单的:若是一个计算不是reactive的,它将被按需处理。就像一个普通的getter函数同样。懒衍生若是没有用了,将被简单的垃圾回收。记住computed values老是须要使用纯函数,由于对于纯函数而言,它是懒衍生仍是直接使用并不重要。在相同的可观察状态下,computed values老是给出相同的结果。
运行计算
Reaction和Computed values在Mobx中都以相同的方式运行。当从新计算被触发时,该函数将被压入到衍生堆栈中。只要计算正在运行,每一个被访问的observable都会将自身注册为衍生堆栈最顶层函数的依赖项。当computed value被须要了,若是该值处于reactive状态,则该值能够简单的是最后已知的值。不然它将push本身到衍生堆栈中,切换到reactive模式并开始计算。
当一个计算完成后,将获得在执行期间访问的可观察列表。在profileView的例子中,这个list将只包含nickName或nickName和fullName属性。任何被移除的属性都将再也不观察(此时computed values可能会从反应模式返回到惰性模式),任何被添加的可观察属性将被观察,直到下一次计算。例如,未来将更改firstName的值,fullName将会知道本身该被从新计算,从而profileView会从新计算。接下来会详细解释这一过程。
Propagating state changes
Figure 5:更改值1对依赖关系树的影响。虚线表示将被标记为旧的观察者。数字表示计算的顺序。
衍生将自动对状态变化作出反应。全部反应同步发生,更重要的是无瑕疵。修改可观察值时,将执行如下算法:
可观察值向全部观察者发送过期通知,代表它已变得陈旧。任何受影响的computed values将以递归方式将通知传递给其观察者。所以,依赖关系树的一部分将被标记为陈旧。以Figure 5为例,当值1改变时观察者将变成陈旧的,并用橘色虚线标记。全部的衍生均可能被变化的值影响。
在发送陈旧通知而且存储新值以后,一个就绪通知将被发送。用于指示该值是否确实发生了变化。
一旦衍生收到步骤1中收到的每一个陈旧通知的就绪通知,它就会知道全部的被观察值都稳定了,因而将开始从新计算。计算 就绪/陈旧 消息的数量将确保这一点。例如,计算值4将仅在计算值3变得稳定后从新计算。
若是没有就绪消息指出一个值变化了,衍生将直接告诉本身的观察者它已经准备好了且没有变化中的值,不然将从新计算并发送一个就绪消息给本身的观察者。执行顺序如Figure 5所示。注意,若是计算值4从新评估但没有产生新值,则最后一个“-”将永远不会执行。
前两段总结了如何在运行时跟踪可观察值和衍生之间的依赖关系以及变化在衍生中是如何传播的。此时你会发现reaction基本上就是一个始终处于反应模式的computed value。**重点:这个算法能够很是有效地实现而不须要闭包,只须要一堆指针数组。**另外,Mobx还应用了许多其余优化,这些优化超出了本文的范围。
同步执行
人们常惊讶于Mobx同步运行全部内容。这有两大好处:第一点是不可能观察陈旧的衍生。所以,在更改影响它的值后,能够当即使用衍生值。第二点是这让追踪堆栈和调试变得简单,它避免了Promise/Async库所特有的无用堆栈跟踪。
transaction(() => {
michel.firstName = "Mich";
michel.lastName = "W.";
});
复制代码
(事务示例,它确保没有人能追踪到像Michaaa这样的中间值)
同步执行还引入了对事务的需求。若是当即连续应用几个突变,在应用全部更改后,最好从新评估全部衍生。在transaction中包装action能实现这个目的。事务推迟全部就绪通知,直到事务块执行完成。请注意,事务仍然同步运行和更新全部内容。
这总结了Mobx最基本的实现细节。这没有涵盖全部内容,可是很高兴你能够组合你的computed value了。经过组合reactive computations,甚至能够自动的将一张数据图转换为另外一张数据图并用最少的补丁数保持最新的衍生,这使得实现复杂模式变得简单。
总结
复杂应用程序的状态最好用图表表示,以实现参考一致性,更接近问题核心的心理模型。
不该该使用手动定义的订阅或游标来强制更改状态,这将不可避免的致使因为订阅不足或超额订阅致使的错误。
使用运行时分析来肯定最小的observer->observable的关系,这致使了一种计算模型,能够保证在没有观察过时值的状况下运行最小量的衍生。
任何不须要实现有效反作用的衍生均可以彻底优化。