Vue3 watchEffect和watch的区别与副作用清理
在 Vue 3 的组合式 API 中,watchEffect 和 watch 是用于监听响应式数据变化并执行副作用的两个核心函数。理解它们的区别及如何正确清理副作用,是编写健壮、高效代码的关键。
核心区别速览
选择哪个函数,取决于你的具体需求。下表总结了它们的最核心区别:
| 特性 | watchEffect |
watch |
|---|---|---|
| 触发方式 | 立即执行,并自动追踪依赖 | 惰性执行(默认),需明确指定监听源 |
| 依赖收集 | 自动。函数内访问的任何响应式数据都会成为依赖 | 手动。必须在第一个参数中明确指定监听源 |
| 获取旧值 | 无法方便地获取变化前的旧值 | 可以。回调函数的参数中包含旧值和新值 |
| 适用场景 | 依赖关系简单,需要副作用立即且自动执行 | 需要精确控制监听源,或需要对比新旧值 |
1. watchEffect:自动追踪与立即执行
watchEffect 会立即执行你传入的函数,并在执行过程中自动追踪所有访问到的响应式依赖。当这些依赖发生变化时,函数会重新执行。
基本用法:
- 导入
watchEffect函数。 - 调用
watchEffect并传入一个副作用函数。
<script setup>
import { ref, watchEffect } from 'vue'
const message = ref('Hello')
const count = ref(0)
// 这个函数会立即执行一次,并自动追踪 `message` 和 `count`
watchEffect(() => {
console.log(`Effect triggered: message is "${message.value}", count is ${count.value}`)
})
</script>
执行流程:
- 组件初始化时,副作用函数立即执行一次。
- 函数内访问了
message.value和count.value,它们被自动收集为依赖。 - 此后,只要
message或count中的任意一个值发生变化,该函数都会被重新执行。
2. watch:精确监听与惰性执行
watch 需要你明确指定一个或多个“监听源”。它默认是惰性的,即不会立即执行回调,直到监听源发生变化。
基本用法:
- 导入
watch函数。 - 调用
watch,第一个参数是监听源,第二个参数是回调函数。
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('等待问题...')
// 监听 `question` ref 的变化
watch(question, (newValue, oldValue) => {
console.log(`旧问题: "${oldValue}"`)
console.log(`新问题: "${newValue}"`)
// 模拟异步操作
setTimeout(() => {
answer.value = `对 "${newValue}" 的回答是: 42`
}, 1000)
})
</script>
```
**关键配置**:
你可以为 `watch` 传递第三个参数,一个配置对象,来改变其行为。
```javascript
const source = ref(0)
watch(
source,
(newVal, oldVal) => {
console.log(`值从 ${oldVal} 变为 ${newVal}`)
},
{
immediate: true, // 立即执行一次回调,以 `source` 的当前值作为初始值
deep: true, // 深度监听对象/数组内部的变化
flush: 'post', // 调整副作用的刷新时机('pre'|'post'|'sync')
}
)
```
---
### 3. 副作用清理机制
当你的副作用函数涉及**异步操作**(如定时器、网络请求、事件监听)时,**清理**这些操作至关重要,以避免内存泄漏和不可预期的行为。`watchEffect` 提供了一种优雅的清理机制。
**原理**:`watchEffect` 的副作用函数可以接收一个 `onCleanup` 回调作为参数。你可以在这个回调里**注册**一个清理函数。Vue 会在以下时机**调用**这个清理函数:
1. 副作用**重新运行**之前(在侦听器回调再次执行之前)。
2. 侦听器**被停止**时(例如,组件卸载)。
**实战示例:清理异步请求**
假设我们需要监听一个 ID,并根据 ID 从 API 获取数据。如果不清理,当 ID 快速变化时,可能会导致前一个未完成的请求的响应覆盖后面请求的结果。
```vue
<script setup>
import { ref, watchEffect } from 'vue'
const id = ref(1)
const data = ref(null)
const loading = ref(false)
watchEffect((onCleanup) => {
// 设置一个标志,用于标识当前这个 effect 是否仍然有效
let cancelled = false
loading.value = true
data.value = null
// 模拟一个异步 API 请求
const fetchPromise = fetch(`/api/data/${id.value}`)
.then((res) => res.json())
.then((json) => {
// 仅当该 effect 未被清理(cancelled 仍为 false)时,才更新数据
if (!cancelled) {
data.value = json
loading.value = false
}
})
// 注册清理函数
onCleanup(() => {
// 将 cancelled 设为 true,通知进行中的请求其结果已失效
cancelled = true
console.log(`清理前一个 ID 为 ${id.value} 的请求`)
})
})
</script>
执行流程解析:
- ID 首次设为
1,watchEffect执行。一个针对1的请求发出,并注册一个将cancelled置为true的清理函数。 - 在请求完成前,ID 被快速改为
2。 - Vue 重新执行
watchEffect。 - 首先,Vue 调用上一次注册的
onCleanup回调,将cancelled设为true。此时,针对1的请求虽然还在进行,但其结果将被忽略。 - 然后,新的
watchEffect函数开始执行,发起针对2的新请求,并注册新的清理函数。 - 组件卸载时,也会执行最后注册的清理函数,确保任何进行中的请求都被妥善处理。
实战示例:清理定时器
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log('定时器执行')
}, 1000)
// 在下一次 effect 运行前或侦听器停止时,清除这个定时器
onCleanup(() => {
clearInterval(timer)
})
})
4. 总结与选择指南
- 选择
watchEffect:当你需要一个“副作用”立即运行,并且其依赖能被函数内部自然追踪时。特别适合封装那些依赖关系简单、执行逻辑内聚的副作用,例如:- 根据响应式状态同步更新文档标题。
- 在 Canvas 上绘制实时数据。
- 一个完整的、包含异步和清理逻辑的请求函数。
- 选择
watch:当你需要以下控制能力时:- 惰性执行:仅在数据实际变化时才执行回调。
- 访问旧值:在回调中需要对比数据的前一个值和新值。
- 精确指定监听源:只监听特定的、多个独立的数据源(可以是多个 ref 或一个返回对象的 getter 函数)。
- 深度监听对象:明确使用
{ deep: true }。
正确使用清理机制是避免 Vue 应用中细微 bug 的关键。始终思考你的副作用是否留下了“尾巴”,并确保在它失效时将其清理干净。

暂无评论,快来抢沙发吧!