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 值之后,可观察值是如何通知观察者进行更新的

相关推荐
好开心3315 分钟前
js高级06-ajax封装和跨域
开发语言·前端·javascript·ajax·okhttp·ecmascript·交互
小镇程序员19 分钟前
vue2 src_Todolist消息订阅版本
前端·javascript·vue.js
Zack No Bug27 分钟前
解决报错:rror: error:0308010C:digital envelope routines::unsupported
前端·javascript·vue.js
凌虚1 小时前
Web 端语音对话 AI 示例:使用 Whisper 和 llama.cpp 构建语音聊天机器人
前端·人工智能·后端
小宇python2 小时前
Web应用安全入门:架构搭建、漏洞分析与HTTP数据包处理
前端·安全·架构
伊泽瑞尔2 小时前
关于什么是前端架构师的讨论
前端·架构
珹洺2 小时前
从 HTML 到 CSS:开启网页样式之旅(二)—— 深入探索 CSS 选择器的奥秘
前端·javascript·css·网络·html
liro2 小时前
CSS盒子模型
前端
热爱前端的小张2 小时前
包管理器
前端
冰冻果冻2 小时前
vue--制作随意滑动的相册
前端·javascript·vue.js