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

相关推荐
熊猫钓鱼>_>15 小时前
从零到一:打造“抗造” Electron 录屏神器的故事
前端·javascript·ffmpeg·electron·node·录屏·record
晚霞的不甘15 小时前
Flutter for OpenHarmony《智慧字典》 App 主页深度优化解析:从视觉动效到交互体验的全面升级
前端·flutter·microsoft·前端框架·交互
我是伪码农15 小时前
Vue 1.28
前端·javascript·vue.js
鹓于15 小时前
Excel一键生成炫彩二维码
开发语言·前端·javascript
siwangdexie_new15 小时前
html格式字符串转word文档,前端插件( html-docx-js )遇到兼容问题的解决过程
前端·javascript·html
2601_9496130216 小时前
flutter_for_openharmony家庭药箱管理app实战+用药提醒列表实现
服务器·前端·flutter
利刃大大16 小时前
【Vue】scoped作用 && 父子组件通信 && props && emit
前端·javascript·vue.js
-凌凌漆-16 小时前
【Vue】Vue3 vite build 之后空白
前端·javascript·vue.js
心柠16 小时前
前端工程化
前端
沐雪架构师16 小时前
核心组件2
前端