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 属性标记依赖的四种状态:

  1. NOT_TRACKING:在执行之前,或事务之外,或未被观察(计算值)时,所处的状态。此时 Derivation 没有任何关于依赖树的信息。枚举值-1
  2. UP_TO_DATE:表示所有依赖都是最新的,这种状态下不会重新计算。枚举值0
  3. POSSIBLY_STALE:计算值才有的状态,表示深依赖发生了变化,但不能确定浅依赖是否变化,在重新计算之前会检查。枚举值1
  4. 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
    })
  }
}

简洁起见,接下来几篇文章引用的源码中都会忽略这部分代码。

编辑于 2017-12-05 10:43