零到一打造 Vue3 响应式系统 Day 16 - 性能处理:LinkPool

昨天,我们完成了"依赖清理"机制,让 effect 能够正确处理动态变化的依赖关系。然而,这也带来了一个新的性能问题:当依赖频繁变化时,系统需要不断地创建和销毁 Link 节点。每次建立依赖关系都会触发内存分配,频繁的分配/释放会导致:

  • 垃圾回收 (GC) 压力增大:GC 执行得越频繁,就越可能造成应用程序的短暂卡顿。
  • 内存碎片化:频繁处理和释放小块内存,可能导致内存空间中出现大量不连续的内存碎片。
  • 性能下降:内存管理本身的开销。

我们可以通过对象池 (Object Pool) 的设计模式来解决这个问题。

Object Pool 设计模式

对象池 (Object Pool) 是一种设计模式,用于管理和复用对象,以避免频繁创建和销毁对象带来的性能损耗。

与其在需要时创建、在用完时销毁,不如将可复用的对象统一管理起来,实现循环利用。 这个对象池就像一个"仓库",预先存放一批可以重复使用的对象。当需要对象时从池中取出,使用完毕后放回到池中,而不是销毁它。

这样可以达到:

  • 复用已分配的内存:避免了大量的内存分配操作。
  • 减少垃圾回收次数:降低对主线程的干扰。

LinkPool 采用单向链表结构,并且依照后进先出 (LIFO) 的原则。主要是因为入池、出池都只需要对头节点进行操作,时间复杂度为 O(1),效率很高。

我们接下来的执行步骤如下:

  • LinkPool 未使用
  • 移除 Link2 节点
  • 移除 Link1 节点
  • 复用 linkPool 中的节点

可以观察一下它们的链表关系以及 LinkPool 的使用情况。

初始化

linkPool 池是空的,什么都还没运行,没有可回收的节点。

移除 Link2

通过 endTrack(sub) 判定有"尾段过期"→ 调用 clearTracking(Link2)Link2 被回收到池中。

移除 Link1

通过 endTrack(sub) 再次判定有"尾段过期"→ 调用 clearTracking(Link1)Link1 被回收到池中,并排在 Link2 前面。

复用 Link1

执行 link(dep, sub),这次 if (linkPool)true ,走复用分支,从池中取出 Link1 进行复用。

LinkPool 代码实现

TypeScript 复制代码
// system.ts

interface Dep {
  subs: Link | undefined
  subsTail: Link | undefined
}

interface Sub {
  deps: Link | undefined
  depsTail: Link | undefined
}

export interface Link {
  sub: Sub
  nextSub: Link
  prevSub: Link
  dep: Dep
  nextDep: Link | undefined
}

let linkPool: Link

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

  let newLink: Link

  /**
   * 查看 linkPool 是否存在,如果存在,表示有可复用的节点
   */
  if (linkPool) {
    newLink = linkPool
    linkPool = linkPool.nextDep // 池指针后移
    newLink.nextDep = nextDep
    newLink.dep = dep
    newLink.sub = sub
  } else {
    /**
     * 如果 linkPool 不存在,表示没有可复用的节点,那就创建一个新节点
     */
    newLink = {
      sub,
      dep,
      nextDep,
      nextSub: undefined,
      prevSub: undefined
    }
  }

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

  if (sub.depsTail) {
    sub.depsTail.nextDep = newLink
    sub.depsTail = newLink
  } else {
    sub.deps = newLink
    sub.depsTail = newLink
  }
}

export function propagate(subs) {
  // ... (不变)
}

export function startTrack(sub) {
  // ... (不变)
}

export function endTrack(sub) {
  // ... (不变)
}

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

    if (prevSub) {
      prevSub.nextSub = nextSub
      link.nextSub = undefined
    } else {
      dep.subs = nextSub
    }

    if (nextSub) {
      nextSub.prevSub = prevSub
      link.prevSub = undefined
    } else {
      dep.subsTail = prevSub
    }

    link.dep = undefined
    link.sub = undefined

    /**
     * 把不再需要的节点放回 linkPool 中,以备复用
     */
    link.nextDep = linkPool
    linkPool = link

    link = nextDep
  }
}

通过对 linkclearTracking 函数的修改,我们完成了 LinkPool 机制。这看起来是一个很小的修改,但实际上是对响应式系统底层的重要性能优化。Link 节点的生命周期从"用完即毁"变成了"循环再生",从根本上解决了因动态依赖而产生的频繁内存分配与回收问题。


想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。

相关推荐
一树山茶3 小时前
uniapp的双token
前端·javascript
正义的大古3 小时前
OpenLayers地图交互 -- 章节六:范围交互详解
前端·javascript·vue.js·openlayers
訾博ZiBo3 小时前
【文本朗读小工具】- 快速、免费的智能语音合成工具
前端
天蓝色的鱼鱼3 小时前
低代码是“未来”还是“骗局”?前端开发者有话说
前端
答案answer4 小时前
three.js着色器(Shader)实现数字孪生项目中常见的特效
前端·three.js
城管不管4 小时前
SpringBoot与反射
java·开发语言·前端
JackJiang4 小时前
即时通讯安全篇(三):一文读懂常用加解密算法与网络通讯安全
前端
一直_在路上4 小时前
Go架构师实战:玩转缓存,击破医疗IT百万QPS与“三大天灾
前端·面试
早八睡不醒午觉睡不够的程序猿4 小时前
Vue DevTools 调试提示
前端·javascript·vue.js