目录
-
背景与引子:为什么要理解 Effect Scope
-
什么是 Effect(副作用)
2.1 纯函数与副作用对比
2.2 Vue 响应式中的副作用示例 -
Effect Scope 出现的原因
-
Vue 组件中的默认 Effect Scope
-
临时 Effect Scope 的应用
5.1 停止、暂停与一次性监听
5.2 临时 Scope 实现once
效果 -
长生命周期 Effect Scope 的应用
6.1 Composable 缓存与共享
6.2 VueUse 的createSharedComposable
实现 -
实际应用场景与最佳实践
7.1 内存管理与资源释放
7.2 复杂业务中的作用域隔离 -
总结与思考
-
参考与延伸阅读
1. 背景与引子:为什么要理解 Effect Scope
Vue 的响应式系统常常被形容为“魔法”。我们写下一句 count.value++
,页面就会自动更新。然而这背后的逻辑离不开 副作用(effect) 和 作用域(scope)。
大多数 Vue 开发者其实一直在用 Effect Scope —— 在 setup()
里写的任何响应式逻辑,实际上都运行在一个作用域里。当组件卸载时,这个作用域会被 stop()
,从而清理内部所有 watcher 和计算属性。
👉 理解它的意义在于:
-
避免内存泄漏
-
精准控制副作用的生命周期
-
更好地设计 composable 与工具函数
2. 什么是 Effect(副作用)
2.1 纯函数与副作用对比
纯函数:只依赖输入,返回输出,不修改外部环境:
function add(a, b) {
return a + b
}
副作用函数:除了返回值,还修改了外部变量:
let counter = 0
function add(a, b) {
counter++
return a + b
}
2.2 Vue 响应式中的副作用
Vue 的计算属性与监听器就是典型的副作用:
const count = ref(0)
const double = computed(() => count.value * 2)
watchEffect(() => console.log(double.value))
count.value = 4 // 触发计算和打印
这里:
-
computed
是一个 effect(依赖追踪 + 自动更新) -
watchEffect
也是一个 effect(监听变化 + 执行副作用)
3. Effect Scope 出现的原因
没有作用域时,watcher 会永远存在,即使组件卸载,也会继续执行。结果就是:
-
内存泄漏(watcher 没被清理)
-
多余的计算(浪费性能)
-
难以排查的 Bug
Effect Scope 提供了解决方案:把相关的副作用打包进一个作用域,随时可以整体 stop。
4. Vue 组件中的默认 Effect Scope
Vue 在每个组件的 setup()
阶段,都会自动创建一个作用域:
<script setup>
// 这里所有的 ref、computed、watchEffect
// 都运行在一个 effectScope 里
</script>
当组件卸载时,Vue 自动调用 scope.stop()
,清理所有 watcher。
5. 临时 Effect Scope 的应用
5.1 停止、暂停与一次性监听
我们常常需要控制 watcher 的生命周期:
// 一次性监听
watch(source, cb, { once: true })
// 暂停 / 恢复
const { pause, resume } = watch(source, cb)
pause()
resume()
// 手动停止
const stop = watch(source, cb)
stop()
问题:这些 watcher 仍然存在于组件的作用域内。
5.2 临时 Scope 实现 once 效果
临时作用域让 watcher 在触发后立即销毁:
const count = ref(0)
const scope = effectScope()
scope.run(() => {
watch(count, () => {
console.log(count.value)
scope.stop() // 触发一次后立即销毁
})
})
count.value = 3 // 打印 3,并清理 watcher
count.value = 6 // 不再触发
这比 { once: true }
更灵活,能精细控制副作用。
6. 长生命周期 Effect Scope 的应用
6.1 Composable 缓存与共享
有些逻辑需要 超越单个组件的生命周期,例如全局 store 或共享数据。
VueUse 提供了 createSharedComposable
,就是基于长生命周期的 effectScope:
function createSharedComposable(fn) {
let scope, cached
return (...args) => {
if (!scope) {
scope = effectScope(true) // detached scope
cached = scope.run(() => fn(...args))
}
return cached
}
}
const useStore = createSharedComposable(() => {
const count = ref(0)
watch(count, (v) => console.log('watch', v))
return { count }
})
这里的作用域不会随着组件卸载而销毁,适合全局复用。
6.2 Detached Scope 的意义
effectScope(true)
创建 独立作用域,不依赖父 scope。这样我们就能手动管理生命周期,避免意外 stop。
7. 实际应用场景与最佳实践
7.1 内存管理与资源释放
-
定时器、订阅、WebSocket 监听,都应该挂到一个 scope 里,方便统一清理。
-
避免“幽灵 watcher”浪费内存。
7.2 复杂业务中的作用域隔离
-
表单编辑场景:每次进入新表单时,可以用一个临时 scope 管理 watcher,退出时直接 stop。
-
可视化编辑器:每个 widget 对应一个 scope,关闭 widget 即销毁 scope,资源隔离清晰。
8. 总结与思考
Effect Scope 是 Vue 响应式系统的幕后基石。
-
它让副作用的生命周期有了“边界感”
-
临时作用域解决短时任务
-
长生命周期作用域支撑全局共享
理解它,你就能写出更稳健的响应式逻辑,避免隐藏 bug 和性能陷阱。
9. 参考与延伸阅读
💡 互动小提示:
这篇文章如果对你有帮助,记得点赞、收藏和评论!
你在项目里有没有遇到过 watcher 没有清理导致的 bug?欢迎分享经验~