通过对象池保存被清理的 Link 节点,减少依赖变化时频繁创建和销毁节点的开销

2025-09-14 13:17:28

前面几篇处理了动态依赖、失效依赖清理,以及 effect 自触发的问题。

但一个性能问题:当依赖频繁变化时,会不断创建和清理 Link 节点。 如果这些被清理掉的节点能被复用,就可以减少对象创建带来的开销。

在依赖发生变化时,旧的依赖会被 clearTracking 清理掉。

但后面如果又收集了新的依赖,link 里还是会重新创建一个新的 Link

const newLink: Link = {
  sub,
  prevSub: undefined,
  nextSub: undefined,

  dep,
  nextDep
}

如果依赖频繁切换,就会不断重复这两个动作:

  • 清理旧的 Link
  • 创建新的 Link

这类场景下,可以用对象池来复用已经废弃的节点。

Object Pool

Object Pool 也就是对象池。

它是一种管理和复用对象的设计模式,用来避免频繁创建和销毁对象带来的性能开销。

这里的思路是:

  • 清理依赖时,不直接丢弃 Link
  • 把废弃的 Link 放进一个池子里
  • 下次需要创建 Link 时,优先从池子里取
  • 池子里没有可复用节点时,再创建新的对象

定义 linkPool

既然要复用节点,就需要有一个地方保存这些废弃的 Link

可以先在 system.ts 中定义一个 linkPool

// system.ts
let linkPool: Link | undefined

清理时放入对象池

上一章的 clearTracking 会负责清理失效依赖。

现在不直接丢弃被清理的 Link,而是把它放进 linkPool

// system.ts
function clearTracking(link: Link) {
  while (link) {
    const { prevSub, nextSub, dep, nextDep } = link

    // 从 dep.subs 链表中移除当前 link

    link.dep = link.sub = undefined
    link.nextDep = linkPool
    linkPool = link

    link = nextDep
  }
}
link.nextDep = linkPool
linkPool = link

这段代码会把当前废弃的 Link 插到对象池的头部。

linkPool 永远指向最近一个被清理出来的可复用节点。

创建时优先复用

复用发生在建立链表节点的时候,也就是 link 函数里。

分成两种情况:

  • linkPool 不存在:说明没有可复用节点,创建新的 Link
  • linkPool 存在:从对象池取出一个节点,并把池指针往后移动
let linkPool: Link | undefined

export function link(dep: ReactiveNode, sub: ReactiveNode) {
  const currentDep = sub.depsTail

  const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
  if (nextDep && nextDep.dep === dep) {
    sub.depsTail = nextDep
    return
  }

  let newLink: Link

  if (linkPool) {
    newLink = linkPool
    linkPool = linkPool.nextDep

    newLink.nextDep = nextDep
    newLink.dep = dep
    newLink.sub = sub
  }
  else {
    newLink = {
      sub,
      prevSub: undefined,
      nextSub: undefined,

      dep,
      nextDep
    }
  }

  // 后续继续执行挂载到 dep.subs 和 sub.deps 的逻辑
}
  1. 复用之前仍然要先判断当前依赖能不能直接复用旧节点:
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
if (nextDep && nextDep.dep === dep) {
  sub.depsTail = nextDep
}

如果当前依赖已经能从 effect.deps 链表里复用,就不需要走对象池。

  1. 从对象池取节点时,要把池指针往后移动:
newLink = linkPool
linkPool = linkPool.nextDep

取出来的节点会被重新赋值:

newLink.nextDep = nextDep
newLink.dep = dep
newLink.sub = sub

这样它就从“废弃节点”变回了当前这次依赖收集需要使用的 Link

整体流程

对象池的整体流程可以这样理解:

  1. 默认情况下,linkPoolundefined
  2. 假设现在有一组依赖节点:link1 <-> link2 <-> link3
  3. link2 被清理时,clearTracking 会把它从原来的依赖链表里移除
  4. 然后执行 link.nextDep = linkPoollinkPool = link
  5. 此时原来的依赖链表变成 link1 <-> link3
  6. 对象池里变成 link2 -> undefined
  7. 如果继续清理 link1,它也会被放到对象池头部
  8. 此时对象池变成 link1 -> link2 -> undefined
  9. 下次需要新建 Link 时,会优先从 linkPool 取出 link1
  10. 取出后,linkPool 往后移动,变成 link2 -> undefined
  11. link1 重新赋值后,再挂回当前新的依赖链表

这样被清理掉的 Link 节点就不会马上变成垃圾对象,而是可以在后续依赖收集中继续复用。