2025.09.13

内存优化:对象池复用 Link 节点的实践

📉 痛点:频繁创建与销毁带来的性能瓶颈

尽管我们实现了完善的依赖清理机制(如前文所述),但在依赖关系频繁变动的场景下,Link 节点的反复创建与销毁仍然是主要的性能杀手。

每当建立一个新的依赖关系时,都需要进行一次内存分配;高频的“申请—释放”循环将引发以下问题:

  • 内存碎片化 (Fragmentation): 短生命周期的小块内存不断被切割、回收,导致堆空间变得零散,不利于后续大块内存的分配。
  • GC 压力陡增 (Garbage Collection Overhead): 短暂存活的对象激增,强制垃圾回收器(GC)更频繁地介入工作,抢占应用执行时间,最终导致应用帧率下降和卡顿。

💡 解决方案

为了实现依赖关系的“零”成本循环利用,我们引入了对象池(Object Pool 模式。

核心思想是:将“用完”的 Link 节点并非直接销毁,而是将其放入一个缓存池 (linkPool) 中等待复用。下次需要 Link 节点时,优先从池中取出。

对象池模式下的生命周期

引入对象池后,Link 节点的生命周期从销毁变为了回收与复用。

阶段操作说明核心机制
1. 初始化linkPool 初始状态为空linkPool === undefined
2. 回收调用 clearTracking 清理过期依赖时,不再接触引用等待垃圾回收, 而是将节点链表头部插入linkPoollink.nextDep = linkPool; linkPool = link
3. 复用调用 link(dep, sub) 尝试建立新的依赖时,先检查 linkPool 是否有可用节点。if (linkPool) { newLink = linkPool; linkPool = linkPool.nextDep; }

本次流程如下

  1. 回收 link2: 在 clearTracking过程中, link2 及其后续节点被归还对象池
  2. 回收 link1: 随后, link1 也被推入对象池, 成为新的池子头部
  3. 再次 link(): 再次执行依赖收集时, 优先从 linkPool 中取出 link1 进行复用, 实现"零"成本创建

💻 代码实现

  1. 在创建新 link 节点前增加了对象池的检查逻辑
export function link(dep, sub) {
  // ... (省略依赖复用判断逻辑)

  let newLink: Link
  if (linkPool) {
    // 🚀 对象池复用:从池中取出头部节点
    newLink = linkPool
    linkPool = linkPool.nextDep // 更新池子头部

    // 重置并赋予新 Link 节点新的上下文
    newLink.nextDep = nextDep
    newLink.dep = dep
    newLink.sub = sub
  }
  else {
    newLink = { /* ... 属性初始化 ... */ }
  }

  // ... (省略双向链表的连接逻辑)
}
  1. 在移除双向链表的引用后, 不再简单丢弃, 而是将节点还给对象池
function clearTracking(link: Link) {
  while (link) {
    const { prevSub, nextSub, nextDep, dep } = link

    // ... (省略从双向链表中移除的代码)

    // 1. 清理上下文引用
    link.dep = link.sub = undefined
    link.nextSub = link.prevSub = undefined

    // 2. ♻️ 节点归还对象池
    link.nextDep = linkPool // 将当前池子头部赋给回收节点的 nextDep
    linkPool = link // 更新池子头部为当前回收节点

    // 3. 移动到下一个待清理节点
    link = nextDep
  }
}