Mobx 源码解读(一) 基本概念
项目中使用 Mobx 有一段时间了,与 Redux 相比,自己最直观的感受就是避免了 Redux 中大量的样板代码。不需要再去写 action creator, reducer 等,应用的状态直接在 Action 内修改,Mobx 会自动管理依赖的更新和副作用的触发。
最近花时间研究了下 Mobx 源码,看看 Mobx 是如何实现优雅的状态管理。通过这几篇文章记录一些自己的理解,作为以后项目中的参考,避免项目中可能踩到的各种坑。
具体分析源码之前有必要先理清 Mobx 中的几个概念:
transaction
事务的概念大家都不陌生,通常表示一组原子性的操作。Mobx 中的事务用于批量处理 Reaction 的执行,避免不必要的重新计算。Mobx 的事务实现比较简单,使用 startBatch 和 endBatch 来开始和结束一个事务:
function startBatch() {
// 通过一个全局的变量 inBatch 标识事务嵌套的层级
globalState.inBatch++
}
function endBatch() {
// 最外层事务结束时,才开始执行重新计算
if (--globalState.inBatch === 0) {
// 执行所有 Reaction
runReactions()
// 处理不再被观察的 Observable
const list = globalState.pendingUnobservations
for (let i = 0; i < list.length; i++) {
const observable = list[i]
observable.isPendingUnobservation = false
if (observable.observers.length === 0) {
observable.onBecomeUnobserved()
}
}
globalState.pendingUnobservations = []
}
}
可以看到,事务可以嵌套,直到最外层事务结束之后,才会重新执行 Reaction。用一张图来形象地表示事务的概念:
例如,一个 Action 开始和结束时同时伴随着事务的启动和结束,确保 Action 中(可能多次)对状态的修改只触发一次 Reaction 的重新执行。
function startAction() {
// ...
startBatch()
// ...
}
function endAction() {
// ...
endBatch()
// ...
}
Atom
任何能用于存储应用状态的值在 Mobx 中称为 Atom,它会在「被观察时」和「自身发生变化时」发送通知。BaseAtom 基类定义了实现「可观察」的几个关键属性和方法:
class BaseAtom implements IAtom {
// 标志属性,不再被观察时为 true
isPendingUnobservation = true
// 观察者数组
observers = []
// 观察者数组的映射
observersIndexes = {}
// 用于比较 Derivation 的新旧依赖
diffValue = 0
// 上一次被使用时,Derivation 的 runId
lastAccessedBy = 0
// 状态最新的观察者所处的状态
lowestObserverState = IDerivationState.NOT_TRACKING
constructor(public name = "Atom@" + getNextId()) {}
public onBecomeUnobserved() {
}
// 被使用时触发
public reportObserved() {
reportObserved(this)
}
// 发生变化时触发
public reportChanged() {
startBatch()
propagateChanged(this)
endBatch()
}
toString() {
return this.name
}
}
ObservableValue 正是继承自 BaseAtom。可以看到,reportObserverd 和 reportChanged 分别调用了 reportObserved 和 propagateChanged 两个方法,这正是 Observable 用于「通知被观察」和「通知自身变化」的两个函数。
Atom 可以说是具有「可观察」功能的最小类型,Mobx 也将它作为 API 导出,让用户能够基于它定制一些可观察的数据类型。
Derivation
Derivation 即能够从当前状态「衍生」出来的对象,包括计算值和 Reaction。Mobx 中通过 Derivation 注册响应函数,响应函数中所使用到的 Observable 称为它的依赖,依赖过期时 Derivation 会重新执行,更新依赖。
IDerivation 接口定义的几个重要属性:
interface IDerivation extends IDepTreeNode {
// 依赖数组
observing: IObservable[]
// 每次执行收集到的新依赖数组
newObserving: null | IObservable[]
// 依赖的状态
dependenciesState: IDerivationState
// 每次执行都会有一个 uuid,配合 Observable 的 lastAccessedBy 属性做简单的性能优化
runId: number
// 执行时新收集的未绑定依赖数量
unboundDepsCount: number
// 依赖过期时执行
onBecomeStale()
}
可见,Observable 和 Derivation 是双向关联的,分别持有对方的引用。
Derivation 通过 dependenciesState 属性标记依赖的四种状态:
- NOT_TRACKING:在执行之前,或事务之外,或未被观察(计算值)时,所处的状态。此时 Derivation 没有任何关于依赖树的信息。枚举值-1
- UP_TO_DATE:表示所有依赖都是最新的,这种状态下不会重新计算。枚举值0
- POSSIBLY_STALE:计算值才有的状态,表示深依赖发生了变化,但不能确定浅依赖是否变化,在重新计算之前会检查。枚举值1
- STALE:过期状态,即浅依赖发生了变化,Derivation 需要重新计算。枚举值2
源码中经常能见到 lowestState 之类的变量,表示的是「状态最新的观察者所处的状态」。
接下来是几个在源码中随处可见的概念。
invariant
Mobx 从 React 借鉴了 invariant,在条件为 false 时抛出错误:
function invariant(check: boolean, message: string, thing?) {
if (!check)
throw new Error("[mobx] Invariant failed: " + message + (thing ? ` in '${thing}'` : ""))
}
还有基于 invariant 的 fail:
function fail(message: string, thing?): never {
invariant(false, message, thing)
throw "X" // unreachable
}
spy, intercept 和 observe
在 Mobx 源码中,经常可以看到为实现 spy, intercept 和 observe 插入的大段代码。
spy 可以监听 Mobx 中发生的所有事件,包括可观察值的变化、Action 的执行、Derivation 的计算等,典型的应用就是 mobx-react-devtools
。
典型的实现 spy 的代码:
// 事件开始前
const notify = isSpyEnabled()
let startTime
if (notify) {
startTime = Date.now()
spyReportStart({
object: this,
type: "reaction",
fn
})
}
// ...
// 事件结束后
if (notify) {
spyReportEnd({
time: Date.now() - startTime
})
}
intercept 和 observe 可以在 observable 变化前后设置钩子函数。intercept 可以在 observable 变化前对该变化做出修改,包括取消该变化,例如:
// ObservableValue 变化时
if (hasInterceptors(this)) {
// intercept 修改变化
const change = interceptChange<IValueWillChange<T>>(this, {
object: this,
type: "update",
newValue
})
// change 为 null,可以取消修改
if (!change) return UNCHANGED
newValue = change.newValue
}
observe 会响应所有的变化,即使处在事务中,例如:
// ObservableValue 的 setNewValue 方法
setNewValue(newValue: T) {
const oldValue = this.value
this.value = newValue
this.reportChanged()
// 立即通知 listeners
if (hasListeners(this)) {
notifyListeners(this, {
type: "update",
object: this,
newValue,
oldValue
})
}
}
简洁起见,接下来几篇文章引用的源码中都会忽略这部分代码。