Vue.js: 侦听器 watch

最后更新:2026-08-26

侦听器(watch / watchEffect)让你监听响应式数据的变化并在变化时执行副作用。computed 用于"派生新值",watch 用于"触发动作"(如发请求、设状态)。

watch 是 Vue 响应式系统的重要组成部分,3 个核心场景:数据变化发 API、表单输入防抖、跨数据同步

1. 你将学到


2. 一个搜索框的 API 性能问题

(2) 痛点:每个键入都发请求,API 崩溃

Alice 在后台实现了一个商品搜索框:

JS
// ❌ 翻车版:每个字符都发请求
const { ref } = Vue

const searchQuery = ref('')

watch(searchQuery, async (newValue) => {
  // 每次输入都发请求
  const results = await fetch(`/api/search?q=${newValue}`)
  searchResults.value = await results.json()
})

API 服务器崩溃了:

后端开发者 Charlie 报告:

"Alice,你的搜索在疯狂轰炸 API。我们需要防抖。等用户停止输入 300ms 后再发请求。"

(2) Vue watch + 防抖解法

JS
const { ref, watch } = Vue

const searchQuery = ref('')
const searchResults = ref([])

// 防抖:300ms 内不变化才发请求
let timeoutId = null
watch(searchQuery, (newValue) => {
  if (timeoutId) clearTimeout(timeoutId)
  timeoutId = setTimeout(async () => {
    const response = await fetch(`/api/search?q=${newValue}`)
    searchResults.value = await response.json()
  }, 300)
})

(3) 收益

实现防抖后:


3. watch() 基本语法

(1) 4 个参数

JS
const { ref, watch } = Vue

const count = ref(0)

watch(
  // 1. 监听源:要监听的响应式数据
  count,
  // 2. 回调函数:(新值, 旧值) => {...}
  (newVal, oldVal) => {
    console.log(`count: ${oldVal} → ${newVal}`)
  },
  // 3. 选项(可选):immediate, deep, flush
  { immediate: true, deep: true }
)

(2) 监听 ref

JS
const count = ref(0)

// 监听 ref
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} → ${newVal}`)
})

(3) 监听多个 ref

JS
const firstName = ref('Alice')
const lastName = ref('Smith')

// 监听多个
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`Name: ${newFirst} ${newLast}`)
})

4. 4 种监听源

(1) 监听 ref

JS
const count = ref(0)
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} → ${newVal}`)
})

(2) 监听 reactive 对象

JS
const { reactive, watch } = Vue

const user = reactive({ name: 'Alice', age: 25 })

// 监听整个 reactive(自动深度)
watch(user, (newVal, oldVal) => {
  console.log('User changed:', newVal)
})

// ✅ 修改内部属性也触发
user.name = 'Bob'  // 触发

(3) 监听 getter 函数

JS
const state = reactive({ user: { profile: { name: 'Alice' } } })

// 只监听特定属性(精确控制)
watch(
  () => state.user.profile.name,
  (newVal) => {
    console.log(`Name: ${newVal}`)
  }
)

(4) 监听 computed

JS
const count = ref(0)
const doubleCount = computed(() => count.value * 2)

watch(doubleCount, (newVal) => {
  console.log(`Double: ${newVal}`)
})

(4) 4 种监听源对比

数据源 写法 是否深度 适用
ref watch(ref, fn) 默认浅 单个 ref
reactive watch(reactive, fn) 自动深 对象
getter watch(() => obj.x, fn) 默认浅 特定属性
computed watch(computed, fn) 默认浅 派生值

5. watchEffect():自动追踪依赖

(1) 基本语法

JS
const { ref, watchEffect } = Vue

const count = ref(0)
const name = ref('Alice')

// 不需要指定监听谁,函数体里用到的响应式数据自动追踪
watchEffect(() => {
  console.log(`count: ${count.value}, name: ${name.value}`)
  // count 变化时重新执行
  // name 变化时也重新执行
})

(2) watchEffect vs watch

维度 watch watchEffect
监听源 手动指定 自动追踪
立即执行 默认否(需 immediate: true ✅ 是
新旧值 ✅ 都有 ❌ 只有新值
清理副作用 onCleanup onCleanup
适用场景 精确监听 通用副作用

(3) watchEffect 的清理

JS
const { watchEffect, onCleanup } = Vue

watchEffect((onCleanup) => {
  // 设置定时器、订阅、事件监听
  const timer = setInterval(() => {
    console.log('tick')
  }, 1000)
  
  // 清理:下次执行前或组件卸载时调用
  onCleanup(() => {
    clearInterval(timer)
  })
})

6. 选项:immediate / deep / flush

(1) immediate:立即触发

JS
const count = ref(0)

// 默认:count 变化才触发
watch(count, (newVal) => console.log(newVal))  // 不会立即执行

// immediate: true:立即触发 1 次
watch(count, (newVal) => console.log(newVal), { immediate: true })
// ✅ 立即执行:0 → 0

(2) deep:深度监听

JS
const { ref, watch } = Vue

const user = ref({ profile: { name: 'Alice' } })

// 默认:ref 包装对象的 .value 整体替换才触发
watch(user, () => console.log('changed'))  // ❌ user.value.profile.name = 'Bob' 不触发

// deep: true:内部属性变化也触发
watch(user, () => console.log('changed'), { deep: true })
// ✅ user.value.profile.name = 'Bob' 触发

(3) flush:触发时机

JS
// 默认 'pre':组件更新前触发
watch(source, fn, { flush: 'pre' })

// 'post':组件更新后触发(DOM 已更新)
watch(source, fn, { flush: 'post' })

// 'sync':同步触发(不常用,性能差)
watch(source, fn, { flush: 'sync' })

7. 完整示例:搜索框 + 防抖

▶ 示例:基础 watch 监听 ref

HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
  <input type="number" v-model.number="count">
  <p>Count: {{ count }}</p>
  <p>查看 console 看变化日志</p>
</div>

<script>
const { createApp, ref, watch } = Vue
createApp({
  setup() {
    const count = ref(0)
    watch(count, (newVal, oldVal) => {
      console.log(`Count changed: ${oldVal} → ${newVal}`)
    })
    return { count }
  }
}).mount('#app')
</script>
▶ 试一试

▶ 示例:防抖搜索(CDN 版模拟)

HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
  <input v-model="searchQuery" placeholder="Search products...">
  <p>Recent query: {{ lastQuery || '(空)' }}</p>
  <p>触发次数: {{ triggerCount }}</p>
</div>

<script>
const { createApp, ref, watch } = Vue
createApp({
  setup() {
    const searchQuery = ref('')
    const lastQuery = ref('')
    const triggerCount = ref(0)

    let timeoutId = null
    watch(searchQuery, (newQuery) => {
      // 清除上一次的定时器
      if (timeoutId) clearTimeout(timeoutId)

      // 设置新的定时器:300ms 后才执行(防抖)
      timeoutId = setTimeout(() => {
        lastQuery.value = newQuery
        triggerCount.value++
      }, 300)
    })

    return { searchQuery, lastQuery, triggerCount }
  }
}).mount('#app')
</script>
▶ 试一试

注:真实项目用 fetch + 后端 API,这里用 setTimeout 模拟触发逻辑。

▶ 示例:监听 reactive 深度变化

HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
  <p>姓名: {{ user.profile.name }}</p>
  <p>年龄: {{ user.profile.age }}</p>
  <p>主题: {{ user.settings.theme }}</p>
  <p>变更计数: {{ changeCount }}</p>
  <button @click="user.profile.name = 'Bob'">改名 Bob</button>
  <button @click="user.settings.theme = 'light'">切换 light</button>
</div>

<script>
const { createApp, reactive, ref, watch } = Vue
createApp({
  setup() {
    const user = reactive({
      profile: { name: 'Alice', age: 25 },
      settings: { theme: 'dark' }
    })
    const changeCount = ref(0)

    // 监听整个 user(reactive 自动深度)
    watch(user, () => { changeCount.value++ })

    return { user, changeCount }
  }
}).mount('#app')
</script>
▶ 试一试

▶ 示例:watchEffect 自动追踪

HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
  <p>count: {{ count }}</p>
  <p>name: {{ name }}</p>
  <p>effect 触发次数: {{ effectCount }}</p>
  <button @click="count++">count++</button>
  <button @click="name = 'Bob'">改名 Bob</button>
</div>

<script>
const { createApp, ref, watchEffect } = Vue
createApp({
  setup() {
    const count = ref(0)
    const name = ref('Alice')
    const effectCount = ref(0)

    // 不指定监听源,函数体用到的自动追踪
    watchEffect(() => {
      console.log(`count: ${count.value}, name: ${name.value}`)
      effectCount.value++   // 立即执行 + 依赖变化时再执行
    })

    return { count, name, effectCount }
  }
}).mount('#app')
</script>
▶ 试一试

▶ 示例:5 个常见错误速查

错误 现象 解决
监听 reactive 不设 deep 内部属性变化不触发 watch(obj, fn, { deep: true })
监听 ref 包装对象整体 内部属性变化不触发 改用 reactive 或加 deep
watchEffect 写无限循环 浏览器卡死 避免修改函数体内用到的数据
没清理定时器 内存泄漏 用 onCleanup
立即执行混淆 想用旧值但没拿到 用 immediate + oldVal(可能 undefined)

❓ 常见问题

Q watch 和 watchEffect 选哪个?
A 能用 watch 选 watch(精确控制 + 新旧值)。watchEffect 适合"我要在某个数据变化时做某事,但不想列出全部依赖"的场景。性能差不多,watchEffect 略慢(要追踪函数体依赖)。
Q watch 监听 ref 时,新旧值是什么类型?
A .value。如果 ref 包装对象,新旧值都是 ref.value(引用相同,除非整个替换)。要监听内部属性变化,用 reactive 或 getter 函数。
Q watch 监听 ref 包装对象,怎么监听内部属性?
A 3 种方式:(1) watch(ref, fn, { deep: true });(2) watch(() => ref.value.x, fn)(getter);(3) 改用 reactive 包装。
Q watch 能监听 props 吗?
A 能。watch(() => props.count, fn)。注意 props 是只读的,watch 回调中不要修改 props。
Q immediate: true 时 oldVal 是什么?
A undefined(首次触发没有"旧值")。可以从 ref 初始值或 store 拿旧值。
Q watch 能异步吗?
A 回调函数可以是 async,但 watch 本身是同步触发的。异步逻辑放在回调里:(newVal) => { setTimeout(() => {...}, 0) }
Q onCleanup 什么时候触发?
A 2 个时机:(1) watch 重新执行前(上一次清理);(2) 组件卸载时。适合清理定时器、取消请求、移除事件监听。

📖 小节


📝 作业

  1. 基础题(难度⭐) 用 watch 监听一个 ref 变量 count

    • count 变化时打印 "count: ${old} → ${new}"
    • immediate: true 让回调立即执行 1 次
  2. 进阶题(难度⭐⭐) 用 watch 实现一个"自动保存"功能:

    • 编辑一个 ref 文本 content
    • 1 秒后自动调用 save(content) 函数(防抖)
    • 切换到其他页面时(组件卸载)清理定时器
  3. 挑战题(难度⭐⭐⭐) 实现一个完整的搜索 + 自动完成 + 历史记录系统:

    1. 防抖搜索(300ms)
    2. 显示搜索结果列表(v-for)
    3. 保留最近 5 次搜索历史
    4. 点击历史项可重新搜索
    5. 切换搜索词时取消未完成的请求
    6. 用 watchEffect + onCleanup 实现
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏