文章目录

Vue3 watchEffect和watch的区别与副作用清理

发布于 2026-05-18 19:22:22 · 浏览 24 次 · 评论 0 条

Vue3 watchEffect和watch的区别与副作用清理

在 Vue 3 的组合式 API 中,watchEffectwatch 是用于监听响应式数据变化并执行副作用的两个核心函数。理解它们的区别及如何正确清理副作用,是编写健壮、高效代码的关键。


核心区别速览

选择哪个函数,取决于你的具体需求。下表总结了它们的最核心区别:

特性 watchEffect watch
触发方式 立即执行,并自动追踪依赖 惰性执行(默认),需明确指定监听源
依赖收集 自动。函数内访问的任何响应式数据都会成为依赖 手动。必须在第一个参数中明确指定监听源
获取旧值 无法方便地获取变化前的旧值 可以。回调函数的参数中包含旧值和新值
适用场景 依赖关系简单,需要副作用立即且自动执行 需要精确控制监听源,或需要对比新旧值

1. watchEffect:自动追踪与立即执行

watchEffect立即执行你传入的函数,并在执行过程中自动追踪所有访问到的响应式依赖。当这些依赖发生变化时,函数会重新执行

基本用法

  1. 导入 watchEffect 函数。
  2. 调用 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>

执行流程

  1. 组件初始化时,副作用函数立即执行一次
  2. 函数内访问了 message.valuecount.value,它们被自动收集为依赖。
  3. 此后,只要 messagecount 中的任意一个值发生变化,该函数都会被重新执行

2. watch:精确监听与惰性执行

watch 需要你明确指定一个或多个“监听源”。它默认是惰性的,即不会立即执行回调,直到监听源发生变化。

基本用法

  1. 导入 watch 函数。
  2. 调用 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>

执行流程解析

  1. ID 首次设为 1watchEffect 执行。一个针对 1 的请求发出,并注册一个将 cancelled 置为 true 的清理函数。
  2. 在请求完成前,ID 被快速改为 2
  3. Vue 重新执行 watchEffect
  4. 首先,Vue 调用上一次注册的 onCleanup 回调,将 cancelled 设为 true。此时,针对 1 的请求虽然还在进行,但其结果将被忽略。
  5. 然后,新的 watchEffect 函数开始执行,发起针对 2 的新请求,并注册新的清理函数。
  6. 组件卸载时,也会执行最后注册的清理函数,确保任何进行中的请求都被妥善处理。

实战示例:清理定时器

watchEffect((onCleanup) => {
  const timer = setInterval(() => {
    console.log('定时器执行')
  }, 1000)

  // 在下一次 effect 运行前或侦听器停止时,清除这个定时器
  onCleanup(() => {
    clearInterval(timer)
  })
})

4. 总结与选择指南

  • 选择 watchEffect:当你需要一个“副作用”立即运行,并且其依赖能被函数内部自然追踪时。特别适合封装那些依赖关系简单、执行逻辑内聚的副作用,例如:
    • 根据响应式状态同步更新文档标题。
    • 在 Canvas 上绘制实时数据。
    • 一个完整的、包含异步和清理逻辑的请求函数。
  • 选择 watch:当你需要以下控制能力时:
    • 惰性执行:仅在数据实际变化时才执行回调。
    • 访问旧值:在回调中需要对比数据的前一个值和新值。
    • 精确指定监听源:只监听特定的、多个独立的数据源(可以是多个 ref 或一个返回对象的 getter 函数)。
    • 深度监听对象:明确使用 { deep: true }

正确使用清理机制是避免 Vue 应用中细微 bug 的关键。始终思考你的副作用是否留下了“尾巴”,并确保在它失效时将其清理干净。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文