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

相关推荐
深念Y几秒前
仿B站项目 前端 3 首页 整体结构
前端·ai·vue·agent·bilibili·首页
IT_陈寒1 分钟前
React 18实战:这5个新特性让我的开发效率提升了40%
前端·人工智能·后端
深念Y6 分钟前
仿B站项目 前端 5 首页 标签栏
前端·vue·ai编程·bilibili·标签栏·trae·滚动栏
克里斯蒂亚诺更新12 分钟前
vue3使用pinia替代vuex举例
前端·javascript·vue.js
Benny的老巢22 分钟前
用 Playwright 启动指定 Chrome 账号的本地浏览器, 复用原账号下的cookie信息
前端·chrome
2501_9418053133 分钟前
从微服务网关到统一安全治理的互联网工程语法实践与多语言探索
前端·python·算法
寧笙(Lycode)35 分钟前
前端包管理工具——npm、yarn、pnpm详解
前端·npm·node.js
小夏卷编程36 分钟前
vue2 实现数字滚动特效
前端·vue.js
文心快码BaiduComate38 分钟前
嫌市面上的刷题App太丑,我让Comate帮我写了个“考证神器”
前端·产品
harrain41 分钟前
html里引入使用svg的方法
前端·svg