首页 / Vue3 入门教程 / 侦听器

Vue3 入门教程

侦听器

本教程共 40 篇 · 第 13 篇 · 更新于 2026-07-29 · 约 8 分钟阅读

VueVue3 入门教程watchwatchEffect侦听器副作用

13. 侦听器

本节目标:学会用 watch 和 watchEffect 在状态变化时执行副作用,掌握深层侦听和立即侦听。

基础用法

计算属性适合声明派生状态。但有些场景需要在状态变化时执行”副作用”——比如操作 DOM、发请求、改其他状态。

Composition API 用 watch 函数:

<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('问题通常包含问号。;-)')
const loading = ref(false)

watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    loading.value = true
    answer.value = '思考中...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = '错误!无法访问 API。' + error
    } finally {
      loading.value = false
    }
  }
})
</script>

watch 第一个参数是响应式源,第二个是回调。源变化时回调执行,回调接收新值和旧值。

侦听源类型

watch 第一个参数可以是:

const x = ref(0)
const y = ref(0)

// 单个 ref
watch(x, (newX) => {
  console.log(`x 是 ${newX}`)
})

// getter 函数
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`x + y 的和是 ${sum}`)
  }
)

// 多个源的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x 是 ${newX},y 是 ${newY}`)
})
Warning

不能直接侦听 reactive 对象的属性:

const obj = reactive({ count: 0 })

// 不行!传的是数字给 watch
watch(obj.count, (count) => {
  console.log(`Count is: ${count}`)
})

要用 getter:

watch(
  () => obj.count,
  (count) => {
    console.log(`Count is: ${count}`)
  }
)

深层侦听

watch 默认是浅层的——只有被侦听的属性被赋新值时才触发,嵌套属性变化不触发。

直接侦听 reactive 对象会隐式创建深层侦听器

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 嵌套属性变化也会触发
})

obj.count++

如果侦听的是返回 reactive 对象的 getter,只有 getter 返回不同对象时才触发:

watch(
  () => state.someObject,
  () => {
    // 只有 state.someObject 被替换时才触发
  }
)

想强制深层侦听,用 deep 选项:

watch(
  () => state.someObject,
  (newValue, oldValue) => { /* ... */ },
  { deep: true }
)
Warning

深层侦听要遍历对象的所有嵌套属性,大数据结构上开销大。只在必要时用,注意性能。

立即侦听

watch 默认是懒的——源变了才回调。想创建时就执行一次,用 immediate: true

watch(
  source,
  (newValue, oldValue) => {
    // 创建时立即执行,之后 source 变化再执行
  },
  { immediate: true }
)

watchEffect

侦听器的回调经常用和源一样的响应式状态。比如:

const todoId = ref(1)
const data = ref(null)

watch(todoId, async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
}, { immediate: true })

todoId 用了两次——一次做源,一次在回调里。watchEffect 可以简化:

watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

回调立即执行,期间自动追踪 todoId.value 作为依赖。todoId.value 变了就重新执行。不需要显式传源。

Tip

watchEffect 只在同步执行期间追踪依赖。异步回调里,只有第一个 await 之前访问的属性才会被追踪。

watch vs watchEffect

watchwatchEffect
依赖追踪只追踪显式源自动追踪回调里所有响应式属性
触发时机源变化才触发创建时立即执行 + 依赖变化触发
控制粒度更精确更方便但不够显式

副作用清理

侦听器里发请求时,如果源在请求完成前变了,旧请求的结果就过时了。理想情况是源变化时取消过时的请求。

onWatcherCleanup()(3.5+)注册清理函数:

import { watch, onWatcherCleanup } from 'vue'

watch(id, (newId) => {
  const controller = new AbortController()

  fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
    // 处理响应
  })

  onWatcherCleanup(() => {
    controller.abort()  // 取消过时请求
  })
})
Note

onWatcherCleanup 必须在 watch 回调或 watchEffect 函数的同步执行期间调用,不能在 await 之后。

也可以用回调的第三个参数 onCleanup

watch(id, (newId, oldId, onCleanup) => {
  // ...
  onCleanup(() => {
    // 清理逻辑
  })
})

停止侦听器

setup()<script setup> 里同步创建的侦听器会自动绑定到组件实例,组件卸载时自动停止。

需要手动停止时,调用返回的函数:

const unwatch = watchEffect(() => {})

// 不再需要时
unwatch()
Important

异步创建的侦听器不会自动停止,必须手动停止避免内存泄漏。尽量同步创建。

本节回顾

  • watch 侦听响应式源,变化时执行回调
  • 源可以是 ref、getter、reactive 对象或多个源的数组
  • 深层侦听用 deep 选项,注意性能
  • immediate: true 创建时立即执行
  • watchEffect 自动追踪依赖,立即执行
  • onWatcherCleanup 清理过时副作用
  • 同步创建的侦听器自动停止