零到一打造 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」,一起跟日安当同学。

相关推荐
_志哥_5 分钟前
解除有些网站不能复制的终极办法
前端·chrome
愚昧之山绝望之谷开悟之坡19 分钟前
什么是uv和传统的区别
前端·chrome·uv
SRC_BLUE_1729 分钟前
NSSCTF - Web | 【第五空间 2021】pklovecloud
android·前端
golang学习记29 分钟前
从0死磕全栈之Next.js 数据安全实战指南:从零信任到安全架构
前端
云中雾丽31 分钟前
flutter中 getx 的使用
前端
Jay丶1 小时前
聊聊入职新公司两个月,试用期没过这件事
前端·面试
ZTeam前端全栈进阶圈1 小时前
Vue新技巧:<style>标签里的 CSS 也能响应式!
前端
ღ_23331 小时前
vue3二次封装element-plus表格,slot透传,动态slot。
前端·javascript·vue.js
摸着石头过河的石头1 小时前
JavaScript继承的多种实现方式详解
前端·javascript
ybb_ymm1 小时前
前端开发之ps基本使用
前端·css