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

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

前文:

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

上文我们从 makeAutoObservable 看到 mobx 实现响应式的过程中,将原有对象的属性删除了,替代的是实例化了一个 ComputedValue 对象,保存到了 ObservableObjectAdministration 上的 values_map 对象上,并将其 get 访问代理成为了 ObservableObjectAdministration.getObservablePropValue_,即 ComputedValue.get,这一章我们看一下 ComputedValue 的源码实现,看看其内部做了哪些事

ComputedValue

class ComputedValue<T> implements IObservable, IComputedValue<T>, IDerivation

首先是其类的定义,实现了 IObservable, IComputedValue, IDerivation 这三个接口,IComputedValue 只定义了实现 get、set 方法,看一下另外两个接口的定义

IObservable

export interface IObservable extends IDepTreeNode {
    // 当 observable 的值发生变化时,diffValue_ 会增加,用于在比较过程中检测是否发生了变化,用于避免不必要的重新计算
    diffValue_: number
    
    // 用于确定最后一次访问此 observable 的派生函数的标识符。如果最后一次访问的派生函数与当前派生函数相同,说明依赖关系已经建立
    lastAccessedBy_: number
    // 标识此 observable 是否正在被观察
    isBeingObserved_: boolean
    // 用于记录可观察值的依赖状态,以避免不必要的状态传播
    lowestObserverState_: IDerivationState_ 
    // 标识是否处于待观察状态,确保在每个批处理中只向 global.pendingUnobservations 推送一次自身
    isPendingUnobservation_: boolean 

    // 存储观察此 observable 的派生函数
    observers_: Set<IDerivation>

    // 在被取消观察时的回调函数
    onBUO(): void
    // 在被观察时的回调函数
    onBO(): void

    // 分别存储在取消观察前和观察时需要调用的回调函数集合,供调用
    onBUOL: Set<Lambda> | undefined
    onBOL: Set<Lambda> | undefined
}

IObservable 作为可观察值定义的基石,定义了作为可观察值的必要因素,如一些标记值、优化用的标识、回调函数和存储 observers_ 的观察者,可以看到要求使用 IDerivation 的实现,再看一下 IDerivation 定义了哪些

IDerivation

export interface IDerivation extends IDepTreeNode {
    // 存储此派生函数当前正在观察的 observable 对象,他们的变化会触发派生函数重新计算
    observing_: IObservable[]
    // 临时记录新的观察对象。计算完成后,将其复制到 observing_ 中。
    newObserving_: null | IObservable[]
    // 用于记录派生函数的依赖状态,以确定是否需要重新计算。具体的依赖状态包括是否变为脏状态等
    dependenciesState_: IDerivationState_
    // 表示当前派生函数的运行标识符,每次追踪时递增
    runId_: number
    // 记录在当前运行中派生函数未绑定的依赖数量。
    unboundDepsCount_: number
    // 当派生函数变为陈旧状态时,即需要重新计算时,会调用此回调函数。用于执行一些清理和通知操作
    onBecomeStale_(): void
    // 当前是否处于追踪模式
    isTracing_: TraceMode
    // for debug,如果派生函数没有依赖项,是否发出警告
    requiresObservable_?: boolean
}

IDerivation 作为观察者定义的基石,同样定义了作为观察者的必要因素,如一些状态标记、回调函数和存储 observing_ 的观察值

从而得知,ComputedValue 不仅仅实现了某一方功能,既是观察者又是被观察者,所以它的成员变量是可观察变量和衍生的集合。

除了这些,ComputedValue 还实现了一些其他属性,如下

// 表示是否正在计算,用来检查循环引用
isComputing_: boolean = false 
// 是否正在 set
isRunningSetter_: boolean = false
// 重点:保存值,受保护
protected value_: T | undefined | CaughtException = new CaughtException(null)
// get 函数,就是 ObservableObjectAdministration.getObservablePropValue_
derivation: () => T
// set 函数
setter_?: (value: T) => void
// target 上下文对象
scope_: Object | undefined
// 比较逻辑,是浅比较还是深比较
private equals_: IEqualsComparer<any>
// 当设置为真时,计算值将会一直保持活跃状态,而不会因为没有观察者而自动清除
keepAlive_: boolean


constructor(options: IComputedValueOptions<T>) {
  if (!options.get) {
      die(31)
  }
  this.derivation = options.get!
  this.name_ = options.name || "ComputedValue"
  if (options.set) {
      this.setter_ = createAction(
          "ComputedValue-setter",
          options.set
      ) as any
  }
  // 配置浅比较还是深比较
  this.equals_ =
      options.equals ||
      ((options as any).compareStructural || (options as any).struct
          ? comparer.structural
          : comparer.default)
  this.scope_ = options.context
  this.keepAlive_ = !!options.keepAlive
}

接着看一下 ComputedValueget 方法,看一下当有观察者观察 ComputedValue 时(就是获取 ComputedValue 的值,即 get)发生了什么,这也是 mobx 能够自动进行依赖收集的关键

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,用于确定是否当前处于响应式上下文
          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
}

这里面有几个关键的函数

  • shouldCompute
  • computeValue_
  • reportObserved
  • trackAndCompute
  • propagateChangeConfirmed

在代码注释里,我只是概括了一下他们所做的事情,在后续章节中将会详细做源码解读。

下一章我们将从组件出发,看一下 react 是如何调用到 ComputedValue.get 然后绑定好依赖关系的

原文链接:https://juejin.cn/post/7329336551006322725 作者:彭祯伟_JetPeng

(0)
上一篇 2024年1月29日 下午4:05
下一篇 2024年1月29日 下午4:16

相关推荐

发表回复

登录后才能评论