2025.09.13
内存优化:对象池复用 Link 节点的实践
📉 痛点:频繁创建与销毁带来的性能瓶颈
尽管我们实现了完善的依赖清理机制(如前文所述),但在依赖关系频繁变动的场景下,Link 节点的反复创建与销毁仍然是主要的性能杀手。
每当建立一个新的依赖关系时,都需要进行一次内存分配;高频的“申请—释放”循环将引发以下问题:
- 内存碎片化 (
Fragmentation): 短生命周期的小块内存不断被切割、回收,导致堆空间变得零散,不利于后续大块内存的分配。 - GC 压力陡增 (
Garbage Collection Overhead): 短暂存活的对象激增,强制垃圾回收器(GC)更频繁地介入工作,抢占应用执行时间,最终导致应用帧率下降和卡顿。
💡 解决方案
为了实现依赖关系的“零”成本循环利用,我们引入了对象池(Object Pool) 模式。
核心思想是:将“用完”的 Link 节点并非直接销毁,而是将其放入一个缓存池 (linkPool) 中等待复用。下次需要 Link
节点时,优先从池中取出。
对象池模式下的生命周期
引入对象池后,Link 节点的生命周期从销毁变为了回收与复用。
| 阶段 | 操作说明 | 核心机制 |
|---|---|---|
| 1. 初始化 | linkPool 初始状态为空 | linkPool === undefined |
| 2. 回收 | 调用 clearTracking 清理过期依赖时,不再接触引用等待垃圾回收, 而是将节点链表头部插入 到 linkPool 中 | link.nextDep = linkPool; linkPool = link |
| 3. 复用 | 调用 link(dep, sub) 尝试建立新的依赖时,先检查 linkPool 是否有可用节点。 | if (linkPool) { newLink = linkPool; linkPool = linkPool.nextDep; } |
本次流程如下
- 回收
link2: 在clearTracking过程中,link2及其后续节点被归还对象池 - 回收
link1: 随后,link1也被推入对象池, 成为新的池子头部 - 再次
link(): 再次执行依赖收集时, 优先从linkPool中取出link1进行复用, 实现"零"成本创建
💻 代码实现
- 在创建新
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 = { /* ... 属性初始化 ... */ }
}
// ... (省略双向链表的连接逻辑)
}
- 在移除双向链表的引用后, 不再简单丢弃, 而是将节点还给对象池
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
}
}