MobX 是最近在 React 社区比较火的状态管理工具。与 Redux 相比,Mobx 简单又神秘。不止是因为 MobX 比较火,MobX 的双向绑定与 Vue 的实现也是非常相似,十分有必要去了解一下双向绑定的实现。这篇文章通过源码解释 MobX 这些奇怪的“特性”。
就像 Redux,MobX 跟 React 也没有关系,我们从最最简单的例子开始。MobX 版本 3.1.7
observable
1 2 3
| const store = observable({ title: 'front end developer', });
|
observable 返回一个新的对象 ref,包含一个 $mobx 属性,$mobx 是一个 ObservableObjectAdministration 对象。这个对象稍微有点复杂,不过其实就是 Object.defineProperty 拦截 getter 和 setter,我们暂不需要弄懂 getter 和 setter 究竟做了什么事情。values.title 是一个 ObservableValue 对象,我们大概可以猜到它的属性 observers 存放了观察 values.title 变化的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| { title: 'front end developer', $mobx: { name: 'ObservableObject@1.user', target: ref, values: { title: { value: 'front end developer', name: 'ObservableObject@1.title', observers: [], get() { this.reportObserved(); return this.value; }, set: function(v) { setPropertyValue(this, propName, v); }, }, }, }, get title: function() { return this.$mobx.values[propName].get(); }, set title: function(v) { setPropertyValue(this, propName, v); }, }
|
上面只是一个最简单的对象,如果对象复杂点,也会递归的包装对象。
autorun
在一个函数中简单输出 store.title,我们跟踪下 autorun 方法。
1 2 3
| autorun(() => { console.log(store.title); });
|
autorun 方法会使用传入的参数创建一个 Reaction 对象 reaction, 然后调用 reaction 的 schedule 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| { name: 'Reaction@2', onInvalidate() { function reactionRunner() { view(reaction); } this.track(reactionRunner); }, observing: [], newObserving: [], dependenciesState: IDerivationState.NOT_TRACKING, __mapid: '#3', diffValue: 0, runId: 0, unboundDepsCount: 0, isDisposed: false, _isScheduled: false, _isTrackPending: false, _isRunning: false, }
|
schedule 方法将 reaction 添加到 pendingReactions,最后执行每个 reaction 的 runReaction 方法。
1 2 3 4 5 6 7 8 9
| runReaction() { startBatch(); this._isScheduled = false; if (shouldCompute(this)) { this._isTrackPending = true; this.onInvalidate(); } endBatch(); }
|
onInvalidate 调用 track, track 调用 trackDerivedFunction,跟踪下 trackingDerivation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function trackDerivedFunction(derivation, f, context) { derivation.runId = ++globalState.runId; const prevTracking = globalState.trackingDerivation; globalState.trackingDerivation = derivation; let result; try { result = f.call(context); } catch (e) { result = new CaughtException(e); } globalState.trackingDerivation = prevTracking; bindDependencies(derivation); return result; }
|
这个函数在执行我们的匿名函数之前,设置全局 trackingDerivation 为当前的 reaction ,执行之后又设置回原来的变量。在我们的匿名函数中打印 store.title,回想下最开始 ObservableValue 对象在解析 store.title 值时会调用 reportObserved 。
1 2 3 4 5 6 7 8 9 10 11
| function reportObserved(observable) { const derivation = globalState.trackingDerivation; if (derivation !== null) { if (derivation.runId !== observable.lastAccessedBy) { observable.lastAccessedBy = derivation.runId; derivation.newObserving[derivation.unboundDepsCount++] = observable; } } else if (observable.observers.length === 0) { queueForUnobservation(observable); } }
|
这儿的 derivation 就是我们的 autorun 创建的 Reaction 对象。到现在我们瞧出了一下端倪,在一个 Reaction 环境中解析值,则该 Reaction 依赖该 observable 对象。注意这儿并没有直接放到 observing 数组中!执行完当前方法之后,在 bindDependencies 才重新设置了 observing。这一步是必需的,考虑下 autorun 中有条件语句的情景,除了条件语句,如果我们的对象稍复杂点,譬如 store.user.title,我们对 store.user 重新赋值就会改变依赖的 observable。最后分析下下面的代码片段,猜测输出几次:
1 2 3 4
| autorun(() => { console.log(store.title); store.title = 'hello world!'; });
|
你会发现输出一次,因为初次执行时reaction.observing 为空,执行完之后才会根据 reaction.newObserving 更新 observing。再在外面修改 store.title = ‘changed title’,这次就会正常的输出 changed title 然后输出 hello world!
bindDependencies 设置新的 observing 后,还同步更新依赖的 ObservableValue 的 observers,store.title 被哪些依赖到需要更新。在 store.title 发生变化时,setPropertyValue 会触发 propagateChanged 方法
1 2 3 4 5 6 7 8 9 10 11 12 13
| function propagateChanged(observable) { if (observable.lowestObserverState === IDerivationState.STALE) return; observable.lowestObserverState = IDerivationState.STALE;
const observers = observable.observers; let i = observers.length; while (i--) { const d = observers[i]; if (d.dependenciesState === IDerivationState.UP_TO_DATE) d.onBecomeStale(); d.dependenciesState = IDerivationState.STALE; } }
|
observable 变化时,调用 observers 中每个对象的 onBecomeStale 方法,对 Reaction 对象来说 onBecomeStale 简单的调用 shedule;对 ComputedValue 对象来说则会执行 propagateMaybeChanged,这儿有些优化如果 Reaction 对象状态不是已经更新( UP_TO_DATE),什么都不做。为说明这个问题,我们造一个例子:
1 2 3 4 5 6 7 8 9 10 11 12
| const store = observable({ a: 3, b: 4, get sum() { return this.a + this.b; }, }); autorun(() => { console.log(store.sum); store.b = 5; }); store.b = 6;
|
上面这个例子只输出一次,注释掉 autorun 中的 store.b = 5 赋值语句,结果当然会输出两次。如果该赋值语句丢在另一个 autorun 中则会输出四次。不知道该如何解释。
结语
MobX 的反应系统不难理解,从图中的线条就能看出来。分析源码可以帮助我们搞明白在pixel paint这个例子中为什么它会这么高效,在以后的项目中我们也可以受到启发。双向绑定主要是依赖收集,理解起来比较简单,但是关于性能优化部分的分析本篇文章没有提及,有兴趣的同学可以深入研究一下。