Vue 3 watch 与 watchEffect:异步请求、深度监听和清理

Vue 3 watch 与 watchEffect:异步请求、深度监听和清理

类型:Vue 官方 Watchers 文档中文解读。示例保持官方 API 的含义,并补充请求竞态的解释;没有把官方示例宣称为本项目实测。Vue 版本差异会影响可用选项,尤其是 onWatcherCleanup() 与 once。[3]

什么时候用 watcher

计算属性适合从已有状态得到一个派生值。watcher 用来在状态变化后执行副作用,例如请求网络、修改 DOM 或更新另一个外部系统。Vue 官方把这两类工作区分开:不要为了计算显示值而滥用 watcher,也不要把异步副作用硬塞进 computed。[3]

watch:显式声明依赖

Composition API 中,watch 的第一个参数是明确的监听源,第二个参数是回调。它不会自动追踪回调内部访问的其他响应式变量,因此触发条件更容易检查。

下面保留官方示例的核心行为:只有问题里出现问号时才请求接口,并在请求期间更新 loading 状态。

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

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)

watch(question, async (newQuestion) => {
  if (!newQuestion.includes('?')) {
    return
  }

  loading.value = true
  answer.value = 'Thinking...'
  try {
    const response = await fetch('https://yesno.wtf/api')
    answer.value = (await response.json()).answer
  } catch (error) {
    answer.value = `Error! Could not reach the API. ${error}`
  } finally {
    loading.value = false
  }
})
</script>

<template>
  <p>Ask a yes/no question:</p>
  <input v-model="question" :disabled="loading" />
  <p>{{ answer }}</p>
</template>

watch 的监听源可以是 ref、computed ref、getter、reactive 对象或多个源组成的数组。[3]

import { reactive, ref, watch } from 'vue'

const x = ref(0)
const y = ref(0)
const state = reactive({ count: 0 })

watch(x, (newX) => {
  console.log('x:', newX)
})

watch(() => x.value + y.value, (sum) => {
  console.log('sum:', sum)
})

watch([x, () => y.value], ([newX, newY]) => {
  console.log(newX, newY)
})

// 不能直接把 reactive 对象的当前数值作为监听源
// watch(state.count, callback)

// 应该传 getter
watch(() => state.count, (count) => {
  console.log('count:', count)
})

最后一组写法很重要:watch(state.count, ...) 在调用时拿到的是一个普通数值,Vue 没有可追踪的 getter;watch(() => state.count, ...) 才保留了依赖关系。

watchEffect:自动收集同步执行期间的依赖

当回调使用的响应式状态和触发条件完全相同时,watchEffect 可以减少重复声明。它会立即执行,并在同步执行期间自动记录访问到的响应式依赖。[3]

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

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

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

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`)
  }

  data.value = await response.json()
})
</script>

异步回调有一个容易忽略的边界:await 之后访问的响应式数据不属于首次同步依赖收集范围。依赖应尽量在第一次 await 之前明确读取,或者改用显式的 watch。[3]

选择方法时可以这样判断:

  • 需要精确知道哪个状态变化才触发:watch。
  • 回调自然使用少量响应式依赖,且希望立即运行:watchEffect。
  • 需要从状态计算一个值:优先 computed,而不是 watcher。

深度监听有成本

watch 默认是浅层监听。替换被监听的值会触发回调,但嵌套对象内部变更不一定触发。直接监听 reactive 对象时,Vue 会隐式建立深度监听;getter 返回一个 reactive 对象时,只有返回对象本身变化才触发,除非显式设置 deep: true。[3]

import { reactive, watch } from 'vue'

const form = reactive({
  profile: {
    name: ''
  }
})

watch(form, () => {
  // form.profile.name 修改时也会触发
})

watch(
  () => form.profile,
  () => {
    // 默认更关注 profile 引用本身是否替换
  },
  { deep: true }
)

深度监听要遍历嵌套属性,大对象上可能产生明显开销。先缩小监听源,只有确实需要时才使用深度监听。Vue 3.5 及以后还支持将 deep 写成最大遍历深度,但项目应先核对实际 Vue 版本。[3]

防止旧请求覆盖新结果

搜索框、路由参数和详情页经常出现这种竞态:用户先请求 A,随后快速切换到 B;如果 A 比 B 晚返回,旧结果可能覆盖新页面。

Vue 官方提供了 watcher 清理机制。onWatcherCleanup() 在 Vue 3.5+ 可用,而且必须在回调同步执行阶段注册;也可以使用 watcher 回调的第三个 onCleanup 参数,它不受同样的同步限制。[3]

import { ref, watch } from 'vue'

const id = ref(1)
const data = ref(null)
const error = ref(null)

watch(id, (newId, _oldId, onCleanup) => {
  const controller = new AbortController()
  let active = true

  fetch(`/api/items/${newId}`, { signal: controller.signal })
    .then((response) => {
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }
      return response.json()
    })
    .then((value) => {
      if (active) {
        data.value = value
      }
    })
    .catch((reason) => {
      if (reason.name !== 'AbortError' && active) {
        error.value = reason
      }
    })

  onCleanup(() => {
    active = false
    controller.abort()
  })
})

AbortController 取消网络请求,active 又避免了某些非 fetch 副作用在清理后继续写入状态。生产代码还应处理重试、缓存、鉴权和组件离开后的用户体验,这些不是 watcher API 自动提供的。

回调执行时机

Vue 会对 watcher 回调进行批处理,避免同步修改多次就重复执行很多次。默认回调发生在父组件更新后、所属组件 DOM 更新前。如果需要访问更新后的组件 DOM,可以使用 flush: 'post';flush: 'sync' 会同步触发,但没有批处理,不适合频繁变化的数组等数据。[3]

import { watch } from 'vue'

watch(source, callback, {
  flush: 'post'
})

这不是单纯的性能开关。它改变了回调观察到的 DOM 时点,读取 DOM 前必须明确选择时机。

watcher 的生命周期

同步创建于组件 setup() 或 <script setup> 中的 watcher 会绑定到组件,在组件卸载时自动停止。若在异步回调中创建 watcher,它不会自动绑定到组件,必须手动保存并停止,否则可能产生内存泄漏。[3]

import { onUnmounted, watchEffect } from 'vue'

let stop = null

function startLater() {
  setTimeout(() => {
    stop = watchEffect(() => {
      // 这是异步创建的 watcher
    })
  }, 1000)
}

onUnmounted(() => {
  stop?.()
})

文章结论

watch 和 watchEffect 都是副作用工具,不是万能状态管理器。精确触发条件、请求取消、深度监听成本、回调执行时机和生命周期清理,决定了它们能否在真实项目里稳定工作。

本文没有在你的博客服务器或任何外部 API 中运行示例。代码中的公共测试接口仅用于解释官方示例结构。

来源

  1. Vue.js 官方文档:Watchers

本文为中文技术解读,API 版本差异请以 Vue 官方文档为准。


Vue 3 watch 与 watchEffect:异步请求、深度监听和清理
https://blog.529808.xyz//archives/vue-watch-watchEffect-guide
作者
念头通达
发布于
2026年10月01日
更新于
2026年10月01日
许可协议