← 返回题目列表

watchEffect 和 watch 有什么区别?

中等 第 22 / 27 题 更新于 2026/07/29
VuewatchEffectwatch响应式

简化版

watch 显式指定监听源,适合精确监听和拿到新旧值;watchEffect 自动收集同步执行期间访问到的响应式依赖,适合依赖明显来自副作用内部的场景。需要可控、可比较、可调试时优先 watch

详细版

watch 像“指定观察哪个值”。

watch(userId, async (id, oldId) => {
  user.value = await fetchUser(id)
})

watchEffect 像“先执行一次,执行过程中用到什么就追踪什么”。

watchEffect(() => {
  console.log(userId.value, keyword.value)
})

两者都能清理副作用,也都能停止监听。区别在于依赖来源、执行时机、新旧值能力和可维护性。面试里要强调:watchEffect 方便,但依赖隐式;复杂业务里滥用会让副作用边界变得模糊。

完整版教学

一、watch 是显式依赖,watchEffect 是自动依赖

watch 的第一参数就是监听源,因此代码读起来很明确。 看到 watch(userId, ...) 就知道只有 userId 改变会触发。 watchEffect 则在回调同步执行期间自动收集读取过的响应式值。

const userId = ref(1)
const keyword = ref('')

watchEffect(() => {
  console.log(userId.value)
  console.log(keyword.value)
})

上面会追踪两个依赖。 如果后面有人在 effect 里多读了 page.value,触发条件也会变化。 这就是自动收集的方便与风险。

二、watch 能拿新旧值,更适合做差异判断

很多业务不是“变了就做”,而是要知道从什么变到什么。 例如路由参数从 1 变到 2 时取消旧请求,或者关键词长度从 2 到 3 时才触发搜索。 这类场景 watch 更直接。

watch(keyword, (newKeyword, oldKeyword) => {
  if (newKeyword.length < 3) return
  console.log(`从 ${oldKeyword} 变为 ${newKeyword}`)
})
能力watchwatchEffect
显式监听源
新旧值通常不用它做
初始是否执行默认否,可 immediate默认立即执行
依赖维护可控方便但隐式

如果一个搜索框输入 5 次,watch 可以准确知道每次变化的新旧值。 watchEffect 更适合“不关心变化来源,只要依赖状态能重新同步”的副作用。

三、watchEffect 默认会立即执行一次

watchEffect 的设计接近“响应式副作用”。 它需要先运行一遍,才能知道依赖有哪些。 因此它默认立即执行。

watchEffect(() => {
  document.title = `用户 ${userId.value}`
})

这段代码在创建时就会设置一次标题,之后 userId 变化再重新设置。 watch 默认不会立即执行,除非配置 immediate: true

watch(userId, loadUser, { immediate: true })

这个差异在请求接口时很常见。 如果你希望进入页面马上请求,然后参数变化再请求,用 watch(..., { immediate: true }) 可读性通常更强。

四、异步回调里的依赖收集有边界

watchEffect 只会收集同步执行阶段访问到的依赖。 如果在 await 之后才访问某个响应式值,它可能不会按你想象的方式被追踪。 这也是它不适合复杂异步流程的原因之一。

watchEffect(async () => {
  console.log(userId.value) // 会被同步收集
  await fetch('/api')
  console.log(page.value) // 不应依赖它触发 effect
})
effect 开始执行
读取 userId -> 收集依赖
遇到 await -> 同步收集阶段结束
await 后读取 page -> 不作为稳定触发依据

如果异步请求依赖多个明确参数,推荐用 watch([userId, page], ...)。 这样依赖列表一眼可见,也便于后续排查。

五、清理副作用是两者都要掌握的重点

监听里经常会发请求、开定时器、绑定事件。 如果新一轮监听触发时旧副作用还没结束,就可能出现竞态或泄漏。 Vue 提供清理函数,让你在下一次执行前清理旧任务。

watch(query, async (q, _old, onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort())

  const res = await fetch(`/api/search?q=${q}`, {
    signal: controller.signal
  })
})

假设用户 1 秒内输入 8 个字符,如果不取消旧请求,较慢的旧响应可能覆盖较新的结果。 清理函数能把“最后一次输入对应最后一次结果”这个规则保护住。

六、选择原则看副作用是否需要精确边界

watchEffect 适合依赖少、逻辑短、天然就是同步副作用的场景。 例如根据多个响应式值同步本地调试日志、标题、简单样式。 watch 适合接口请求、复杂判断、需要新旧值、需要明确来源的场景。

记忆钩子:想省事用 watchEffect,想可控用 watch;面试和工程里,可控通常更值钱。

不要把 watchEffect 当成“高级版 watch”。 它更像自动依赖版 effect,简洁但隐式。 业务越复杂,越要偏向显式。

七、常见误区与追问

  • 误区:watchEffect 一定比 watch 更高级。 它只是自动收集依赖,复杂业务里显式 watch 往往更可维护。
  • 误区:watchEffect 能稳定追踪异步回调里所有依赖。 它主要收集同步执行阶段访问到的响应式依赖。
  • 误区:watch 默认会立即执行。 watch 默认等待监听源变化,立即执行需要配置 immediate: true
  • 追问:什么时候需要清理函数? 请求取消、定时器、事件监听、Observer 等副作用都需要清理,避免竞态和泄漏。
  • 追问:监听多个值怎么写? 可以用 watch([a, b], ([newA, newB], [oldA, oldB]) => {})
  • 追问:接口请求更推荐哪个? 多数情况下推荐 watch,因为依赖、时机和清理逻辑更明确。

八、加强记忆

这题抓“显式”和“自动”两个词就稳了。watch 指明监听谁,能拿新旧值,适合请求和复杂逻辑;watchEffect 先跑一遍并自动收集同步依赖,适合短小副作用。回答末尾补清理函数和异步依赖边界,会比只背 API 更扎实。