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

相关推荐
IT_陈寒12 小时前
React 18并发渲染实战:5个核心API让你的应用性能飙升50%
前端·人工智能·后端
科普瑞传感仪器12 小时前
从轴孔装配到屏幕贴合:六维力感知的机器人柔性对位应用详解
前端·javascript·数据库·人工智能·机器人·自动化·无人机
n***F87512 小时前
SpringMVC 请求参数接收
前端·javascript·算法
wordbaby12 小时前
搞不懂 px、dpi 和 dp?看这一篇就够了:图解 RN 屏幕适配逻辑
前端
程序员爱钓鱼12 小时前
使用 Node.js 批量导入多语言标签到 Strapi
前端·node.js·trae
鱼樱前端12 小时前
uni-app开发app之前提须知(IOS/安卓)
前端·uni-app
V***u45312 小时前
【学术会议论文投稿】Spring Boot实战:零基础打造你的Web应用新纪元
前端·spring boot·后端
i听风逝夜13 小时前
Web 3D地球实时统计访问来源
前端·后端
iMonster13 小时前
React 组件的组合模式之道 (Composition Pattern)
前端
北辰alk13 小时前
Vue 中 nextTick 的魔法:为什么它能拿到更新后的 DOM?
vue.js