将依赖收集和派发更新从 RefImpl 中抽离出来,让 ref 只负责保存值

2025-09-14 13:17:28

上一篇已经用双向链表解决了多个 effect 被覆盖的问题。

不过现在的 RefImpl 还是承担了太多职责:

  • 保存实际值
  • 收集依赖
  • 维护链表
  • 派发更新

这些逻辑都挤在 get valueset value 里,代码会越来越难维护。 这一篇把依赖收集和派发更新拆出去,让 RefImpl 只保留和 ref 自身强相关的逻辑。

重构目标

让每个模块只做一件事。

  • RefImpl:保存值,并在读取和修改时触发依赖收集和更新派发
  • trackRef:负责依赖收集
  • triggerRef:负责派发更新
  • system.ts:负责底层链表操作

抽离 trackRef 和 triggerRef

先把 RefImpl 里的链表逻辑挪到两个函数里:

interface ReactiveNode {
  subs?: Link
  subsTail?: Link | undefined
}

class RefImpl implements ReactiveNode {
  [ReactiveFlags.IS_REF] = true

  subs: Link | undefined
  subsTail: Link | undefined

  constructor(value) {
    this._value = value
  }

  _value

  get value() {
    if (activeSub) {
      trackRef(this)
    }

    return this._value
  }

  set value(newValue) {
    if (Object.is(newValue, this._value)) {
      return
    }

    this._value = newValue
    triggerRef(this)
  }
}

RefImpl 不再直接关心链表怎么追加,也不再关心订阅者怎么执行。

读取时调用 trackRef(this),修改时调用 triggerRef(this)

/**
 * 创建 ref 和 effect 之间的订阅关系
 */
export function trackRef(dep: ReactiveNode) {
  const newLink = {
    sub: activeSub,
    prevSub: undefined,
    nextSub: undefined
  }

  if (dep.subsTail) {
    dep.subsTail.nextSub = newLink
    newLink.prevSub = dep.subsTail
    dep.subsTail = newLink
  }
  else {
    dep.subs = newLink
    dep.subsTail = newLink
  }
}

/**
 * 让 ref 对应的 effect 重新执行
 */
export function triggerRef(dep: ReactiveNode) {
  const queuedEffects = []
  let link = dep.subs

  while (link) {
    queuedEffects.push(link.sub)
    link = link.nextSub
  }

  queuedEffects.forEach(effect => effect())
}

这样已经比上一篇更清晰了,但 trackReftriggerRef 里仍然包含大量链表细节。

后续 reactive 也要收集依赖,也会需要同样的追加和遍历逻辑,所以还可以继续拆。

抽离 system.ts

新建一个 system.ts,专门存放依赖系统里的通用结构和链表操作:

// packages/reactivity/src/system.ts
export interface Link {
  sub: Function
  prevSub: Link | undefined
  nextSub: Link | undefined
}

export interface ReactiveNode {
  subs?: Link
  subsTail?: Link | undefined
}

export function link(dep: ReactiveNode, sub: Function) {
  const newLink: Link = {
    sub,
    prevSub: undefined,
    nextSub: undefined
  }

  if (dep.subsTail) {
    dep.subsTail.nextSub = newLink
    newLink.prevSub = dep.subsTail
    dep.subsTail = newLink
  }
  else {
    dep.subs = newLink
    dep.subsTail = newLink
  }
}

export function propagate(subs: Link) {
  const queuedEffects = []
  let link: Link | undefined = subs

  while (link) {
    queuedEffects.push(link.sub)
    link = link.nextSub
  }

  queuedEffects.forEach(effect => effect())
}

这里的两个函数职责更底层:

  • link:把订阅者追加到依赖节点上
  • propagate:从头节点开始遍历,并执行收集到的订阅者

system.ts 不关心这个依赖节点来自 ref 还是其它响应式对象。 它只处理依赖节点和订阅者之间的关系。

改造 ref.ts

有了 system.ts 后,ref.ts 里的逻辑就可以继续简化:

// packages/reactivity/src/ref.ts
import type { Link, ReactiveNode } from './system'
import { activeSub } from './effect'
import { link, propagate } from './system'

enum ReactiveFlags {
  IS_REF = '__v_isRef'
}

class RefImpl<T> implements ReactiveNode {
  [ReactiveFlags.IS_REF] = true

  private _value: T
  subs: Link | undefined
  subsTail: Link | undefined

  constructor(value: T) {
    this._value = value
  }

  get value() {
    if (activeSub) {
      trackRef(this)
    }

    return this._value
  }

  set value(newValue) {
    if (Object.is(newValue, this._value)) {
      return
    }

    this._value = newValue
    triggerRef(this)
  }
}

export function ref<T>(value: T) {
  return new RefImpl(value)
}

export function trackRef(dep: ReactiveNode) {
  if (activeSub) {
    link(dep, activeSub)
  }
}

export function triggerRef(dep: ReactiveNode) {
  if (dep.subs) {
    propagate(dep.subs)
  }
}

现在 trackReftriggerRef 也只剩下和 ref 相关的入口逻辑:

  • 有当前激活的订阅者时,才调用 link
  • 当前节点有订阅者时,才调用 propagate

真正的链表操作已经被隔离到 system.ts 里。

将 effect 改成 ReactiveEffect

现在还有一个问题:订阅者 sub 保存的是普通函数。

普通函数可以执行,但它本身很难承载更多状态。 比如:

  • 这个 effect 当前是否正在执行
  • 这个 effect 依赖了哪些响应式数据

所以把 effect 包装成一个类:

// packages/reactivity/src/effect.ts

export let activeSub: ReactiveEffect | undefined

export class ReactiveEffect {
  constructor(public fn) {
  }

  run() {
    activeSub = this

    try {
      return this.fn()
    }
    finally {
      activeSub = undefined
    }
  }
}

export function effect(fn: Function) {
  const e = new ReactiveEffect(fn)
  e.run()
}

之前的 activeSub 是一个函数,现在它变成了一个 ReactiveEffect 实例。

对应地,派发更新时也要从直接调用函数,改成调用实例上的 run

export function propagate(subs: Link) {
  const queuedEffects: ReactiveEffect[] = []
  let link: Link | undefined = subs

  while (link) {
    queuedEffects.push(link.sub)
    link = link.nextSub
  }

  queuedEffects.forEach(effect => effect.run())
}

这样 effect 就不只是一个可执行函数了,而是一个可持续扩展的类