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

相关推荐
程序员爱钓鱼4 小时前
Rust 变量与不可变性:为什么默认不能修改变量?
前端·后端·rust
随风一样自由5 小时前
【前端工程化】前端工程化解耦实践:从问题分析到架构重构
前端·重构·架构
vortex55 小时前
Web 渗透测试:未授权与越权漏洞全流程挖掘思路
前端
我有满天星辰5 小时前
【前端模块化】前端模块化演进之路:从脚本堆砌到标准统一
前端
我有满天星辰6 小时前
Vue 3 响应式双雄:ref 与 reactive 完全指南
前端·javascript·vue.js
随风一样自由14 小时前
【前端独角兽】刚进入前端领域的前端开发用jQuery还是Vue3?
前端·javascript·vue3·jquery
IMPYLH15 小时前
HTML 的 <data> 元素
前端·html
YHHLAI15 小时前
[特殊字符] 面试中的 Promise —— 从入门到精通
前端·javascript·面试
leoZ23116 小时前
Claude 驱动的全栈开发:Spring Boot + Vue 的 BS 架构实践
vue.js·spring boot·架构
kyriewen17 小时前
我扒了 GPT-5.6 的全部编程跑分——有一项 OpenAI 没敢放出来
前端·gpt·ai编程