关于Vue 3的watch函数返回的停止函数如何工作

这个问题问得非常刁钻,触及到了 Vue 3 响应式系统的"心脏"。

一句话回答:watch 返回的 stop 函数本质上是一个"取消订阅"函数,它会把当前这个侦听器(Effect)从所有被它依赖的响应式数据的订阅列表中移除,并标记为失效,这样后续数据变化时就不会再触发回调了。


🔍 底层原理(分三步讲透)

第一步:watch 内部创建了一个"响应式副作用(Effect)"

当你调用 watch(sum, callback) 时,Vue 内部会创建一个 ReactiveEffect 实例,这个实例包含了:

  • 你要执行的回调函数 callback
  • 当前是否激活的标志 active: true
  • 一个依赖集合(记录它依赖了哪些响应式数据)

然后,Vue 会立即执行一次回调 (如果配置了 immediate,否则会延迟执行)。在执行过程中,callback 里访问了 sum.value,于是 Vue 的响应式系统就开始"追踪":

  • sum 这个 ref 会把当前正在执行的 Effect(即这个 watch 创建的 Effect)**记录到自己的订阅列表(subscribers)**里。
  • 这样,sum 就知道:"有个 Effect 在依赖我,等我一变,就要通知它。"

依赖关系图如下:

复制代码
sum (ref)  →  订阅列表: [ watchEffect的Effect实例 ]

第二步:sum 变化时,触发 Effect 重新执行

当你修改 sum.value 时,sum 的 setter 会遍历自己的订阅列表,找到所有依赖它的 Effect,然后调度执行这些 Effect 的回调函数。

所以你的 callback 会执行,打印日志,并检查 newValue > 10


第三步:调用 stopWatch() 做了什么?

stopWatchwatch 返回的函数,它的核心逻辑是:

javascript 复制代码
// 伪代码
function stop() {
  if (effect.active) {
    // 1. 从所有依赖它的响应式数据的订阅列表中移除自己
    cleanup(effect)
    // 2. 标记为无效
    effect.active = false
  }
}

具体来说:

  1. 清理依赖 :遍历 Effect 记录的所有依赖(即它访问过的 sum 等),从它们的订阅列表中删除当前 Effect。
    • 相当于 sum 的订阅列表里不再有这个 Effect 了。
  2. 标记失效 :把 Effect 的 active 标志设为 false

之后,无论 sum 如何变化,它的 setter 遍历订阅列表时,都找不到这个 Effect,所以回调永远不会再执行


🎯 关键点:为什么回调中能调用 stopWatch

你的代码里,stopWatch 是在回调内部被调用的。这依赖于两个机制:

  1. 闭包stopWatch 是在外层作用域声明的常量,回调函数(内部函数)可以访问到它。
  2. 执行顺序watch同步返回 的,也就是说,const stopWatch = watch(...) 这行代码执行完,stopWatch 就已经被赋值为一个有效的停止函数了。然后,Vue 才会在合适的时机(如初次执行或数据变化后)执行回调。所以当回调内部第一次执行并调用 stopWatch 时,stopWatch 已经存在,不会出现 undefined 的情况。

⚠️ 注意:如果 watch 配置了 immediate: true,那么回调会在 watch 返回之前立即执行一次。此时 stopWatch 还未赋值,所以回调内调用 stopWatch 会报错。但你的代码没有 immediate,所以是安全的。


📌 总结

维度 解释
stop 的本质 取消订阅函数,从依赖图中移除 Effect
底层机制 响应式系统维护"数据 → Effect"的依赖图,stop 执行"删除订阅"操作
为什么回调内可调用 闭包捕获了 stopWatch,且 watch 是同步返回的,执行顺序正确
执行后效果 后续 sum 变化不再触发回调,因为 sum 的订阅列表里已经没有这个 Effect 了

🔥 额外补充:watchwatchEffect 的停止函数原理完全相同

无论是 watch 还是 watchEffect,返回的停止函数都基于同一个 ReactiveEffect 机制。区别只是依赖收集的方式不同(watch 明确指定依赖源,watchEffect 自动收集),但停止的逻辑是一样的。