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

相关推荐
user94051035547172 小时前
Uniapp 3D 轮播图 轮播视频 可循环组件
前端
前端付豪2 小时前
12、为什么在 <script> 里写 export 会报错?
前端·javascript
Junsen2 小时前
electron窗口层级与dock窗口列表
前端·electron
一个小潘桃鸭2 小时前
需求:el-upload加上文件上传进度
前端
梦醒繁华尽2 小时前
使用vue-element-plus-x完成AI问答对话,markdown展示Echarts展示
前端·javascript·vue.js
鹏多多3 小时前
关于React父组件调用子组件方法forwardRef的详解和案例
前端·javascript·react.js
吃饺子不吃馅3 小时前
AntV X6 核心插件帮你飞速创建画布
前端·css·svg
葡萄城技术团队3 小时前
SpreadJS 纯前端表格控件:破解中国式复杂报表技术文档
前端
Humbunklung4 小时前
C# 压缩解压文件的常用方法
前端·c#·压缩解压