通过对象池保存被清理的 Link 节点,减少依赖变化时频繁创建和销毁节点的开销
2025-09-14 13:17:28
前面几篇处理了动态依赖、失效依赖清理,以及 effect 自触发的问题。
但一个性能问题:当依赖频繁变化时,会不断创建和清理 Link 节点。 如果这些被清理掉的节点能被复用,就可以减少对象创建带来的开销。
频繁创建 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不存在:说明没有可复用节点,创建新的LinklinkPool存在:从对象池取出一个节点,并把池指针往后移动
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 的逻辑
}
- 复用之前仍然要先判断当前依赖能不能直接复用旧节点:
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
if (nextDep && nextDep.dep === dep) {
sub.depsTail = nextDep
}
如果当前依赖已经能从 effect.deps 链表里复用,就不需要走对象池。
- 从对象池取节点时,要把池指针往后移动:
newLink = linkPool
linkPool = linkPool.nextDep
取出来的节点会被重新赋值:
newLink.nextDep = nextDep
newLink.dep = dep
newLink.sub = sub
这样它就从“废弃节点”变回了当前这次依赖收集需要使用的 Link。
整体流程
对象池的整体流程可以这样理解:
- 默认情况下,
linkPool是undefined - 假设现在有一组依赖节点:
link1 <-> link2 <-> link3 - 当
link2被清理时,clearTracking会把它从原来的依赖链表里移除 - 然后执行
link.nextDep = linkPool和linkPool = link - 此时原来的依赖链表变成
link1 <-> link3 - 对象池里变成
link2 -> undefined - 如果继续清理
link1,它也会被放到对象池头部 - 此时对象池变成
link1 -> link2 -> undefined - 下次需要新建
Link时,会优先从linkPool取出link1 - 取出后,
linkPool往后移动,变成link2 -> undefined link1重新赋值后,再挂回当前新的依赖链表
这样被清理掉的 Link 节点就不会马上变成垃圾对象,而是可以在后续依赖收集中继续复用。