实现 computed,并梳理它作为 Dep、Sub 以及脏检查的工作方式

2025-09-27 05:05:12

一个最简单的使用:

const count = ref(0)

const c = computed(() => count.value + 1)

effect(() => {
  c.value
})

count.value++

computed 做了什么呢

双重身份:既是 Dep,也是 Sub

computed 它同时扮演两种角色。

  • Dep:作为依赖项,被其他响应式副作用订阅
  • Sub:作为订阅者,在执行自己的 getter 时收集依赖

computed 在求值时会读取 count.value,所以它是 Sub;而 effect 又会读取 c.value,所以 computed 自己也会变成 Dep

理解了这一点后,后面的实现思路就比较清楚了。

先实现一个基础版 computed

在使用时,computed 有两种写法:

const c = computed(() => count.value)

computed({
  get() {
  },
  set() {
  }
})

所以可以先写一个基础签名:

import type { Link, ReactiveNode } from './system'
import { isFunction } from '@vue/shared'

import { ReactiveFlags } from './constant'

class ComputedRefImpl<T = any> implements ReactiveNode {
  [ReactiveFlags.IS_REF] = true
  subs?: Link
  subsTail?: Link
  deps?: Link
  depsTail?: Link

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

  _value: T | undefined

  get value() {
    this._value = this.fn()
    return this._value
  }

  set value(newValue: T) {
    if (this.setter) {
      this.setter(newValue)
    }
    else {
      console.warn('Write operation failed: computed value is readonly')
    }
  }
}

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 = any>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T> | undefined
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = () => {
    }
  }
  else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  return new ComputedRefImpl(getter, setter)
}

这时可以先写一个最基础的测试:

it('computed ', () => {
  const count = ref(0)

  const c = computed(() => count.value + 1)
  effect(() => {
    console.log('c.value', c.value)
  })

  count.value = 1
})

到这一步,代码已经能工作,但它还不是一个真正意义上的 computed

因为现在每次访问 .value 都会直接执行 getter,还没有实现惰性求值。

让 computed 作为 Dep

effect 读取 c.value 时,应该把当前副作用和 computed 建立关联。

也就是说,computed 要先具备 Dep 的能力。

可以在 get value() 中完成依赖收集:

import { activeSub } from './effect'
import { link } from './system'

get
value()
{
  this._value = this.fn()
  if (activeSub) {
    link(this, activeSub)
  }
  return this._value
}

这样一来,谁读取了 computed.value,谁就会订阅这个 computed

让 computed 作为 Sub

只做到上一步还不够,因为 computed 自己也要去订阅别的响应式数据。

它在执行 getter 的过程中,会访问到像 count.value 这样的响应式值,所以它也必须像 effect 一样,在执行期间完成依赖收集。

get
value()
{
  this.update()
  if (activeSub) {
    link(this, activeSub)
  }
  return this._value
}

update()
{
  const prevSub = activeSub

  setActiveSub(this)
  startTrack(this)

  try {
    this._value = this.fn()
  }
  finally {
    endTrack(this)
    setActiveSub(prevSub)
  }
}

这样在 update() 执行期间,computed 就会作为当前活跃的订阅者,把 getter 中访问到的响应式数据都收集起来。

为什么会报错

改完之后,执行过程中会出现一个错误:

问题出在 propagate

原本我们默认 subs 链表中的节点都是普通副作用,所以会直接调用 notify

export function propagate(subs: Link) {
  const queuedEffect = []
  let linkNode = subs
  while (linkNode) {
    const sub = linkNode.sub
    if (!sub.tracking) {
      queuedEffect.push(sub)
    }
    linkNode = linkNode.nextSub
  }
  queuedEffect.forEach(effect => effect.notify())
}

computed 并没有 notify,它是通过 update 驱动重新计算的。

所以这里要把 computed 和普通 effect 区分开:

export function propagate(subs: Link) {
  const queuedEffect = []
  let linkNode = subs
  while (linkNode) {
    const sub = linkNode.sub
    if (!sub.tracking) {
      if ('update' in sub) {
        // 如果存在 update 方法,说明这里是 computed
        processComputedUpdate(sub as ComputedRefImpl)
      }
      else {
        queuedEffect.push(sub)
      }
    }
    linkNode = linkNode.nextSub
  }
  queuedEffect.forEach(effect => effect.notify())
}

function processComputedUpdate(sub) {
  sub.update() // 执行计算属性
  propagate(sub.deps) // 通知 subs 链表上的所有 sub 重新执行
}

到这里,结构上已经能跑通了,但测试还是没通过。

还需要 dirty 脏检查

问题的根源在于:当前实现里,每次读取 .value 都会触发 update()

get
value()
{
  this.update()
  // ...
}

这意味着它根本没有缓存能力,也没有惰性。

所以要引入一个 dirty 标记,只有值变脏了才重新计算。

不过还有一个细节:即使依赖发生变化,也不代表 computed 的结果一定变化。

比如:

const c = computed(() => count * 0)

无论 count 怎么变,结果始终都是 0。这时如果还继续无脑触发下游更新,就会产生不必要的执行。

所以可以换一个思路:先把副作用标记为脏,再决定是否需要继续派发。

先在副作用上增加一个脏标记:

export class ReactiveEffect {
  // ...
  dirty = false // 是否需要重新计算
}

然后在更新和结束追踪时加入脏检查逻辑:

// system.ts
export function propagate(subs: Link): void {
  // ...
  if (!sub.tracking && !sub.dirty) {
    sub.dirty = true
    if ('update' in sub) {
      processComputedUpdate(sub as ComputedRefImpl)
    }
    else {
      queuedEffect.push(sub)
    }
  }
}

function processComputedUpdate(sub: ComputedRefImpl) {
  if (sub.subs && sub.update()) {
    sub.update()
    propagate(sub.subs)
  }
}

export function endTack(sub: ReactiveNode) {
  sub.tracking = false
  const depsTail = sub.depsTail
  sub.dirty = false // 执行完毕后恢复为干净状态
  // ...
}

这样 computed 才真正具备了两个关键特性:

  • 惰性求值:只有访问时才计算
  • 缓存能力:依赖没变脏时直接复用上一次结果

完整代码

// computed.ts
import { hasChanged, isFunction } from '@vue/shared'
import { ReactiveFlags } from './constant'
import { activeSub, setActiveSub } from './effect'
import { endTack, link, Link, ReactiveNode, startTrack } from './system'

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 class ComputedRefImpl<T = any> implements ReactiveNode {
  [ReactiveFlags.IS_REF] = true

  _value: T

  subs: Link
  subsTail: Link

  deps: Link | undefined
  depsTail: Link | undefined

  tracking = false

  dirty = true // 计算属性脏不脏

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

  get value() {
    if (this.dirty) {
      this.update()
      this.dirty = false
    }

    if (activeSub) {
      link(this, activeSub)
    }

    return this._value
  }

  set value(newValue: T) {
    if (this.setter) {
      this.setter(newValue)
    }
    else {
      console.warn('Write operation failed: computed value is readonly')
    }
  }

  update() {
    const prevSub = activeSub

    setActiveSub(this)
    startTrack(this)
    try {
      const oldValue = this._value
      this._value = this.fn()
      return hasChanged(this._value, oldValue)
    }
    finally {
      endTack(this)
      // 执行完成, 恢复之前的 effect
      setActiveSub(prevSub)
    }
  }
}

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
}
// system.ts
export function propagate(subs: Link): void {
  let linkNode = subs
  const queuedEffect = []
  while (linkNode) {
    const sub = linkNode.sub
    if (!sub.tracking && !sub.dirty) {
      sub.dirty = true
      if ('update' in sub) {
        // computed
        processComputedUpdate(sub as ComputedRefImpl)
      }
      else {
        queuedEffect.push(sub)
      }
    }
    linkNode = linkNode.nextSub
  }
  queuedEffect.forEach(effect => effect.notify())
}

function processComputedUpdate(sub: ComputedRefImpl) {
  if (sub.subs && sub.update()) {
    sub.update()
    propagate(sub.subs)
  }
}