2025.09.19

computed的设计和实现:双重角色的响应式节点

💡 核心概念:computed 的双重身份

在响应式系统中,computed 不仅仅是一个简单的计算函数,它是一个拥有 双重身份 的特殊响应式节点:

  1. 订阅者 (Sub):它订阅其 getter 函数内部访问到的所有响应式变量(如 ref)。
  2. 发布者 (Dep):它向所有访问其 .valueeffect (或下游 computed) 发布更新信号。
角色订阅 (Sub)发布 (Dep)
computed收集其内部 getter 依赖的所有响应式数据 (count)。收集所有访问其 .valueeffect (effect 函数)。

<script type="module">
  // import { ref, effect, computed } from '../../../node_modules/vue/dist/vue.esm-browser.prod.js'
  import {ref, effect, computed} from '../dist/reactivity.esm.js'

  const count = ref(0)
  const c = computed(() => { /* 1. 订阅 count */
    return count.value + 1
  })
  effect(() => { /* 2. 订阅 c */
    console.log('state.a ==> ', c.value)
  })
  // ...
</script>

更新流程:

  1. count 变更 -> 通知 computed
  2. computed 重新计算 -> 通知 effect
  3. effect 重新执行

🧱 统一接口:ReactiveNode 的诞生

为了统一处理 refeffectcomputed 这三类响应式实体,我们将 DepSub 签名统一为 ReactiveNode 接口。

/**
 * 响应式节点统一接口 (ref, effect, computed 均实现此接口)
 */
export interface ReactiveNode {
  // 作为 依赖项 (Dep) 时使用:关联其订阅者 (effect/computed)
  subs?: Link | undefined
  subsTail?: Link | undefined

  // 作为 订阅者 (Sub) 时使用:关联其依赖项 (ref/computed)
  deps?: Link
  depsTail?: Link

  // 标记是否正在追踪依赖,避免重复收集
  tracking?: boolean
}

/**
 * 链表节点,用于连接 Dep 和 Sub 的桥梁
 */
export interface Link {
  dep: ReactiveNode
  sub: ReactiveNode // 保存 effect 或 computed
  prevSub: Link | undefined
  nextSub: Link | undefined
  nextDep: Link | undefined
}

⚙️ computed 函数与 ComputedRefImpl 实现

  1. computed函数: 参数归一化
    computed 函数负责处理传入的 getter 函数或包含 get/set 的对象,创建 ComputedRefImpl 实例。
    import { isFunction } from '@vue/shared'
    // ... (其他必要的导入)
    
    export type ComputedGetter<T> = (oldValue?: T) => T
    export type ComputedSetter<T> = (newValue: T) => void
    
    export interface WritableComputedOptions<T, S = T> {
      get: ComputedGetter<T>
      set: ComputedSetter<S>
    }
    
    export function computed<T>(getter: ComputedGetter<T>)
    export function computed<T, S = T>(options: WritableComputedOptions<T, S>)
    
    export function computed<T>(
      getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
    ) {
      let getter: ComputedGetter<T>
      let setter: ComputedSetter<T> | undefined
    
      if (isFunction(getterOrOptions)) {
        getter = getterOrOptions
      }
      else {
        getter = getterOrOptions.get
        setter = getterOrOptions.set
      }
    
      // 创建核心实现类实例
      const cRef = new ComputedRefImpl(getter, setter)
      return cRef as any
    }
    
  2. ComputedRefImpl: 核心双重角色实现

ComputedRefImpl 类实现了 ReactiveNode 接口,同时处理 Sub 的依赖收集和 Dep 的值读取。

import { activeSub, endTack, link, setActiveSub, startTrack } from './system'
import { Link, ReactiveNode } from './types'

export class ComputedRefImpl<T = any> implements ReactiveNode {
  // 用于 isRef 检查
  [ReactiveFlags.IS_REF] = true
  _value: T // 缓存计算结果

  // 作为 Dep:关联订阅者 (effect)
  subs: Link | undefined
  subsTail: Link | undefined

  // 作为 Sub:关联依赖项 (ref/computed)
  deps: Link | undefined
  depsTail: Link | undefined
  tracking = false

  constructor(
    public fn: ComputedGetter<T>,
    private readonly setter: ComputedSetter<T> | undefined
  ) {
  }

  /**
   * 角色一:作为 Dep (发布者)
   * 当 effect 读取 .value 时,进行依赖收集。
   */
  get value() {
    this.update() // 确保值是最新计算的

    // 如果存在活动的 effect,则建立当前 computed 到该 effect 的依赖关系
    if (activeSub) {
      link(this, activeSub)
    }

    return this._value
  }

  /**
   * 处理可写计算属性的 set 操作
   */
  set value(newValue: T) {
    if (this.setter) {
      this.setter(newValue)
    }
    else {
      console.warn('Write operation failed: computed value is readonly')
    }
  }

  /**
   * 角色二:作为 Sub (订阅者)
   * 当其依赖项 (如 ref) 发生变化时被调用,重新计算值并收集新的依赖。
   */
  update() {
    const prevSub = activeSub

    // 将当前 computed 实例设为 activeSub,以收集其 getter 内部的依赖
    setActiveSub(this)
    startTrack(this)
    try {
      this._value = this.fn() // 重新执行 getter 函数,收集依赖
    }
    finally {
      endTack(this) // 依赖收集结束,清理 deps 链表
      setActiveSub(prevSub) // 恢复之前的 effect 上下文
    }
  }
}

🚨 修复传播机制:区分 effectcomputed

但是这时候会发现这样一个错误:

setTimeout 触发 count.value = 1 后,ref 会调用 propagate。由于 propagate 期望所有订阅者都有一个统一的执行接口(如 run()notify()),而 ComputedRefImpl 只有 update(),因此导致了错误。

修改 propagate 函数,通过检查订阅者实例上是否存在 update 方法来区分 computedeffect

export function propagate(subs: Link): void {
  let linkNode = subs
  const queuedEffect: ReactiveEffect[] = [] // 用于存储需要异步执行的 effect

  while (linkNode) {
    const sub = linkNode.sub as ReactiveNode

    // 忽略正在追踪依赖的订阅者
    if (!sub.tracking) {
      if ('update' in sub) {
        // 🚀 命中 ComputedRefImpl:立即处理更新
        processComputedUpdate(sub as ComputedRefImpl)
      }
      else {
        // 🎯 命中 ReactiveEffect:将其放入队列等待调度
        queuedEffect.push(sub as ReactiveEffect)
      }
    }

    linkNode = linkNode.nextSub
  }

  // 批量处理 queuedEffect (通过调度器 notify)
  queuedEffect.forEach(effect => effect.notify())
}

ref 通知 computed 更新时,computed 应该立即更新自身的值,并随后通知其下游的所有订阅者。

// system.ts
function processComputedUpdate(sub: ComputedRefImpl): void {
  sub.update() // 1. 立即计算 computed 的新值

  // 2. 将 computed 自身作为 Dep,通知所有下游订阅者 (如 effect) 更新
  if (sub.subs) {
    propagate(sub.subs)
  }
}

通过这一关键的接口区分和递归传播,我们成功地将 computed 集成到响应式系统中:computed 像水泵一样,在收到上游依赖通知后,立即执行自身计算,并将更新信号传递给下游。