Mobx 源码解析(四):Mobx 是如何进行依赖收集/双向绑定的

Hi!我是 Jet ,我准备在最近更新系列关于状态管理的源码解读(包含 redux、mobx、jotai),从平时开发中常用的 api 延展,欢迎关注

前文:

Mobx 源码解析(一):从 makeAutoObservable 出发看响应式原理

Mobx 源码解析(二):ComputedValue 初见响应式基石

Mobx 源码解析(三):看看 observer 高阶组件都做了什么👀

在上一章我们看到了 observer 高阶组件里通过创建 Reaction 对象来建立 react 组件和 mobx 的关联,这一章我们深入 Reaction,看看他是如何做这件事的

首先回顾一下 observer 做了哪些与相关 Reaction 的操作(代码见上一章):可以看到只有两个相关的方法,一个是实例化 Reaction,另一个是 Reaction.track

Reaction

先看 constructor 和定义

typescript 复制代码
class Reaction implements IDerivation, IReactionPublic

constructor(
  public name_: string = "Reaction",
  private onInvalidate_: () => void,
  private errorHandler_?: (error: any, derivation: IDerivation) => void,
  public requiresObservable_?
) {}

Reaction 作为观察者,实现了 IDerivation 接口,实例化的时候,传递的 name_ 就是组件的 nameonInvalidate_ 就是组件的 forceRender

再看一下 Reaction.track,这里将执行 react 组件的方法传入了 track

Reaction.track

typescript 复制代码
track(fn: () => void) {
  if (this.isDisposed_) {
      return
  }
  startBatch()
  this.isRunning_ = true
  const prevReaction = globalState.trackingContext
  // trackingContext 代表当前正在运行的 reaction 或 computed value,这里赋值为当前 reaction,后续会使用
  globalState.trackingContext = this
  const result = trackDerivedFunction(this, fn, undefined)
  globalState.trackingContext = prevReaction
  this.isRunning_ = false
  this.isTrackPending_ = false
  // 如果当前已经被废弃,清除依赖关系
  if (this.isDisposed_) {
      clearObserving(this)
  }
  if (isCaughtException(result)) {
      this.reportExceptionInDerivation_(result.cause)
  }
  endBatch()
}

Reaction.track 里进行的操作是开启批处理、标记 isRunning_,然后保存自身到 globalState.trackingContext(后续会用到),调用 trackDerivedFunction ,我们看一下这个方法

trackDerivedFunction

typescript 复制代码
export function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
  const prevAllowStateReads = allowStateReadsStart(true)
  // 将 derivation 置为无须更新的状态
  changeDependenciesStateTo0(derivation)
  // 预分配数组分配+深度变化的空间,数组将被 bindDependencies 裁剪
  derivation.newObserving_ = new Array(derivation.observing_.length + 100)
  derivation.unboundDepsCount_ = 0
  derivation.runId_ = ++globalState.runId
  const prevTracking = globalState.trackingDerivation
  // trackingDerivation 表示 mobx 全局当前正在运行的派生函数
  // 存储为当前的 derivation (reaction) 后续用
  globalState.trackingDerivation = derivation
  globalState.inBatch++
  let result
  if (globalState.disableErrorBoundaries === true) {
      result = f.call(context)
  } else {
      try {
          // 重点:执行 react 函数,让组件去访问 mobx 的值,就会触发 computed value 的 get 方法
          // 提前说一下后续 computed value 会通过 trackingContext or trackingDerivation 来拿到当前的观察者,重点关注这两个值
          result = f.call(context)
      } catch (e) {
          result = new CaughtException(e)
      }
  }
  globalState.inBatch--
  globalState.trackingDerivation = prevTracking
  bindDependencies(derivation)
  allowStateReadsEnd(prevAllowStateReads)
  return result
}

trackDerivedFunction 里当前 derivation (就是 Reaction) 保存到 globalState.trackingDerivation 这个就是后续 computed value 用来确定是哪个观察者在观察自己的值,然后就去调用执行 react 组件,让组件去访问 mobx 的值,就会触发 computed valueget 方法,这时候我们回到 Computed.get 方法,不清楚 Computed 细节的的可以看我的这篇文章,回顾一下代码

Computed.get

typescript 复制代码
public get(): T {
  // 检测循环引用
  if (this.isComputing_) {
      die(32, this.name_, this.derivation)
  }
  // 判断当前值是否有人观察,可不可以跳过这次计算
  if (
      globalState.inBatch === 0 &&
      this.observers_.size === 0 &&
      !this.keepAlive_
  ) {
      // 查看当前值的依赖状态,判断是否需要重新计算当前值
      if (shouldCompute(this)) {
          startBatch()
          this.value_ = this.computeValue_(false)
          endBatch()
      }
  } else {
      // 上报有人观察改值
      reportObserved(this)
      // 查看当前值的依赖状态,判断是否需要重新计算当前值
      if (shouldCompute(this)) {
          // 当前正在运行的 reaction 或 computed value,用于确定是否当前处于响应式上下文
          // 就是 reaction.track 里设置的 reaction
          let prevTrackingContext = globalState.trackingContext
          // 如果设置了 keepAlive_ 并且没有 prevTrackingContext,这表示当前的计算值需要保持活动状态
          if (this.keepAlive_ && !prevTrackingContext) {
              globalState.trackingContext = this
          }
          // 计算前后的值是否相等,如果不相等,发生 change 
          if (this.trackAndCompute()) {
              // change 后通知所有观察此值的派生函数,更改他们的状态
              propagateChangeConfirmed(this)
          }
          globalState.trackingContext = prevTrackingContext
      }
  }
  const result = this.value_!

  if (isCaughtException(result)) {
      throw result.cause
  }
  return result
}

这时会进入到 reportObserved 函数,看一下代码

reportObserved

typescript 复制代码
export function reportObserved(observable: IObservable): boolean {
  // 取出当前的观察者,即之前 trackDerivedFunction 里设置的 reaction
  const derivation = globalState.trackingDerivation
  if (derivation !== null) {
      // 检查上一次访问这个可观察对象时是否使用了相同的runId,相同代表依赖关系已经建立
      if (derivation.runId_ !== observable.lastAccessedBy_) {
          observable.lastAccessedBy_ = derivation.runId_
          // 重点:观察者绑定可观察值,reaction 绑定了这个 computed value
          derivation.newObserving_![derivation.unboundDepsCount_++] = observable
          if (!observable.isBeingObserved_ && globalState.trackingContext) {
              observable.isBeingObserved_ = true
              // 执行有人观察的回调函数
              observable.onBO()
          }
      }
      return observable.isBeingObserved_
  } else if (observable.observers_.size === 0 && globalState.inBatch > 0) {
      // 用于后续检测此观察值是否会被废弃
      queueForUnobservation(observable)
  }

  return false
}

因为之前绑定过 reactiontrackingDerivation,这里可观察值 computed value 就可以知道是哪个观察者在访问自己,从而进行绑定,将当前值添加到观察者的观察列表之中,即

typescript 复制代码
derivation.newObserving_![derivation.unboundDepsCount_++] = observable

这时候,就完成了观察者绑定可观察值这个过程了,我们还需要操作可观察值绑定观察者

等到 react 组件调用完成后,我们回到 trackDerivedFunction,会调用 bindDependencies,在这里将完成可观察值绑定观察者的操作,看一下代码

bindDependencies

typescript 复制代码
// 比较 newObserving 和之前的 observing,去除重复的依赖和不需要的依赖,并绑定新添加的依赖,完成可观察值对观察者的绑定
function bindDependencies(derivation: IDerivation) {
  // 先暂存当前 derivation 之前的观察值列表
  const prevObserving = derivation.observing_
  const observing = (derivation.observing_ = derivation.newObserving_!)
  let lowestNewObservingDerivationState = IDerivationState_.UP_TO_DATE_

  // 遍历 newObserving,因为可能有重复的依赖,所以使用 diffValue_ 来标记,保证每个依赖只被添加一次
  let i0 = 0,
      l = derivation.unboundDepsCount_
  for (let i = 0; i < l; i++) {
      const dep = observing[i]
      if (dep.diffValue_ === 0) {
          dep.diffValue_ = 1
          if (i0 !== i) {
              observing[i0] = dep
          }
          i0++
      }

      if ((dep as any as IDerivation).dependenciesState_ > lowestNewObservingDerivationState) {
          lowestNewObservingDerivationState = (dep as any as IDerivation).dependenciesState_
      }
  }
  // 这时候已经完成了 observing 的更新,里面的依赖都是唯一的,i0 长度之外的依赖都是无意义的了
  observing.length = i0
  // 置空
  derivation.newObserving_ = null

  // 遍历旧的依赖,查看依赖有没有被上面的过程置为 1,如果没有,说明当前 derivation 已经不再依赖这个值了,需要让可观察值去掉当前 derivation
  // 然后再将之前所有的依赖 diffValue_ 置为 0
  l = prevObserving.length
  while (l--) {
      const dep = prevObserving[l]
      if (dep.diffValue_ === 0) {
          // 可观察值去掉当前 derivation
          removeObserver(dep, derivation)
      }
      dep.diffValue_ = 0
  }
  // 这时候已经去掉了不需要的依赖

  // 这时候再去遍历新的 observing 列表,这里的列表有可能是之前就有的依赖,也有可能是新添加的依赖,之前就有的依赖(可观察值)已经绑定过当前 derivation 了,我们不需要再管,只用绑定这次新添加的
  // 因为上面的循环中,我们已经将所有之前就有的依赖的 diffValue_ 置为 0 了,所以只有 diffValue_ 为 1 的依赖没有绑定当前 derivation ,给他绑定一下即可
  while (i0--) {
      const dep = observing[i0]
      if (dep.diffValue_ === 1) {
          dep.diffValue_ = 0
          addObserver(dep, derivation)
      }
  }
  // 这里完成了可观察值与观察者的绑定关系,依赖收集完成

  // 在此派生计算中,一些新观察的派生可能在此过程中变为陈旧,它们没有机会传播陈旧状态,现在触发一下
  if (lowestNewObservingDerivationState !== IDerivationState_.UP_TO_DATE_) {
      derivation.dependenciesState_ = lowestNewObservingDerivationState
      derivation.onBecomeStale_()
  }
}

代码具体的操作我的注释写的很详细,里面做了三次循环,来概括一下就是:

  • 第一次:将上次执行调用 react 组件收集到的依赖进行去重
  • 第二次:查看之前的依赖,排除掉已经不再需要的依赖
  • 第三次:查看有没有依赖没有进行绑定,如果没有,进行可观察值绑定观察者的操作

这时候,我们就已经完成了观察者与可观察值的双向绑定,mobx 完成了依赖收集

总结

总结一下绑定的过程:

  • react 组件创建关联 reaction
  • reaction 保存到全局,执行 react 组件访问 mobx
  • 触发 computed valueget 方法,可观察值从全局拿到当前观察者 reaction
  • 观察者绑定可观察值,即 reaction.newObserving_ 添加 observable
  • react 组件执行完毕,去重和废弃无用依赖后,遍历 newObserving_ 让可观察值绑定观察者,即 observable.observers_.add(node)

下一章我们将会讲解更新了 mobx 值之后,可观察值是如何通知观察者进行更新的

相关推荐
天天向上102413 分钟前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y28 分钟前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁35 分钟前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry35 分钟前
Fetch 笔记
前端·javascript
拾光拾趣录37 分钟前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟37 分钟前
vue3,你看setup设计详解,也是个人才
前端
Lefan41 分钟前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson1 小时前
青苔漫染待客迟
前端·设计模式·架构
写不出来就跑路1 小时前
基于 Vue 3 的智能聊天界面实现:从 UI 到流式响应全解析
前端·vue.js·ui
OpenTiny社区1 小时前
盘点字体性能优化方案
前端·javascript