从零到一打造 Vue3 响应式系统 Day 17 - 性能处理:无限循环

在打造响应式系统时,一个容易遇到的状况是,effect 在执行期间同时"读取"又"写入"同一个依赖,这会造成自我触发 (self-trigger)。

effect 为了读值而被追踪为依赖,但它在同一次执行中又修改了同一个值,导致立即再次触发自己,形成无限循环。

可以看下面的示例

HTML 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <style>
      body {
        padding: 150px;
      }
    </style>
  </head>
<body>
  <div id="app"></div>
  <script type="module">
    // import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    import { ref, effect } from '../dist/reactivity.esm.js'

     const count = ref(0)

      effect(() => {
        // console.log(count++) // 错误的写法,会引发无限循环
        console.log(count.value++) // 正确的写法应该是 count.value++,但这里为了演示问题
      })
  </script>
</body>
</html>

你打开控制台,会看到浏览器因为栈溢出而崩溃:

问题分析

JavaScript 复制代码
effect(() => {
  count.value++  // 这里有两个操作!
})

实际上等同于:

JavaScript 复制代码
effect(() => {
  const currentValue = count.value;  // 1. 读取 count (收集依赖)
  count.value = currentValue + 1;    // 2. 修改 count (触发更新)
})
  • 读取 count.value :这会触发依赖收集,将当前的 effect 注册为 count 的订阅者。
  • 修改 count.value :这会触发更新,响应式系统会遍历 count 的所有订阅者并执行它们。由于 effect 自身就是订阅者,它会被重新执行,从而形成了自我触发的无限循环。

无限循环的流程

同一个 effect 在追踪期间读取了 count,又立刻写入了 count,使自己被再度排入执行队列;这个"读→写→再排队"的节奏每一轮都发生一次,因此形成无限循环。

解决方法

Vue 3 使用 tracking 标记来防止同一个 effect 在其自身执行期间,被重复加入到更新队列中:

代码实现

  • effect.ts

    TypeScript 复制代码
    // effect.ts
    import { Link, startTrack, endTrack } from './system'
    
    export let activeSub;
    
    export class ReactiveEffect {
      // ...
      tracking = false // 是否正在执行(收集中)
      // ...
    }
  • system.ts

    TypeScript 复制代码
    // system.ts
    // ...
    export function propagate(subs) {
      let link = subs
      let queuedEffect = []
    
      while (link) {
        const sub = link.sub
    
        // 只有不在执行中的 effect 才加入队列
        if (!sub.tracking) {
          queuedEffect.push(sub)
        }
        link = link.nextSub
      }
    
      queuedEffect.forEach(effect => effect.notify())
    }
    
    /**
     * 开始追踪,将 depsTail 设为 undefined
     */
    export function startTrack(sub) {
      sub.depsTail = undefined
      sub.tracking = true // 标记为正在执行
    }
    
    /**
     * 结束追踪,找到需要清理的依赖
     */
    export function endTrack(sub) {
      sub.tracking = false // 执行结束,取消标记
      // ... (依赖清理逻辑)
    }

如果我们没有 tracking 机制,effect 在读 count 时被收集,写 count 时又触发自己,接着再执行自己,永远停不下来。通过增加这个简单的状态标记,我们就有效地切断了这种自我触发的无限循环。


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

相关推荐
IT_陈寒4 分钟前
Redis缓存击穿把我整不会了,原来还有这手操作
前端·人工智能·后端
idcu26 分钟前
深入 Lyt.js 组件系统:L2 渲染引擎层的核心
前端·typescript
这是程序猿1 小时前
Spring Boot自动配置详解
java·大数据·前端
文心快码BaiduComate1 小时前
干货|Comate Harness Engineering工程实践指南
前端·后端·程序员
还有多久拿退休金1 小时前
一张栈的图,治好你面试答不出 script 阻塞的病
前端·javascript
光辉GuangHui1 小时前
Agent Skill 也需要测试:如何搭建 Skill 评估框架
前端·后端·llm
To_OC1 小时前
我终于搞懂 Claude Code 核心逻辑!90%的人都用错了模式
前端·ai编程
蓝宝石的傻话1 小时前
Headless浏览器的隐形陷阱:为什么你的AI自动化工具抓不到页面早期错误?
前端
irving同学462381 小时前
Node 后端实战:JWT 认证与生产级错误处理
前端·后端
莽夫搞战术1 小时前
【Google Stitch】AI原生画布重新定义设计,让想法变成可交互界面
前端·人工智能·ui