Vue 2 与 Vue 3 侦听器完全指南:从 Options API 到组合式 API 的演进

前言

在之前的博客中,我们聊了 Vue 的指令、响应式 API(ref/reactive),以及从 Options API 到 Composition API 的演进。今天,我们把目光投向 Vue 中另一个核心概念——侦听器(watch)

watch 是 Vue 中用于观察和响应数据变化的机制。当需要在数据变化时执行异步操作开销较大的操作时,watch 是最有力的工具。无论是搜索框的实时搜索、表单联动的级联更新,还是根据路由变化重新加载数据,都离不开 watch

本文将系统梳理 Vue 2 和 Vue 3 中 watch 的用法差异、底层原理、实战案例和最佳实践,帮你彻底掌握这个 Vue 开发中的核心工具。

一、为什么需要 watch?

在 Vue 中,数据变化驱动视图更新是核心机制。但有些场景下,我们需要在数据变化时执行自定义逻辑,而不是仅仅更新视图:

  • 搜索框实时搜索:用户输入关键词变化时,自动发起 API 请求获取搜索结果
  • 表单联动:选择省份时,自动加载该省份的城市列表
  • 深度监听复杂对象:监听表单对象的某个字段变化后自动保存
  • 路由参数变化:监听 $route 变化,重新加载页面数据
  • 数据持久化:监听数据变化,自动保存到 localStorage

watch vs computed 的选择:计算属性(computed)用于派生数据,具有缓存特性;侦听器(watch)用于执行副作用,适合异步请求、DOM 操作等场景。两者各有分工,不可互相替代。

二、Vue 2 中的 watch(Options API)

2.1 基本用法

在 Vue 2 中,watch 是一个选项对象,键是要观察的属性名(支持点号分隔的路径),值是对应的回调函数或包含选项的对象。

函数式写法(适用于简单监听):

export default {
  data() {
    return {
      searchKeyword: ''
    }
  },
  watch: {
    // 当 searchKeyword 发生变化时,这个函数将被调用
    searchKeyword(newVal, oldVal) {
      console.log(`搜索词从 "${oldVal}" 变为 "${newVal}"`)
      // 执行搜索逻辑
      this.fetchSearchResults(newVal)
    }
  },
  methods: {
    fetchSearchResults(keyword) {
      // 发起 API 请求
    }
  }
}

对象式写法(适用于复杂监听,支持更多配置):

export default {
  data() {
    return {
      user: {
        name: '张三',
        profile: {
          age: 25,
          city: '深圳'
        }
      }
    }
  },
  watch: {
    user: {
      handler(newVal, oldVal) {
        console.log('用户信息变化:', newVal)
        this.syncUserData(newVal)
      },
      deep: true,      // 深度监听对象内部变化
      immediate: true  // 立即执行,组件初始化时触发一次
    }
  }
}

2.2 监听对象的特定属性

使用点号分隔的字符串作为键名,可以监听对象中某个具体属性的变化:

export default {
  data() {
    return {
      user: {
        name: '张三',
        age: 25
      }
    }
  },
  watch: {
    // 只监听 user 对象的 name 属性
    'user.name': function(newVal, oldVal) {
      console.log(`姓名从 "${oldVal}" 变为 "${newVal}"`)
    }
  }
}

2.3 选项详解:deep 和 immediate

选项 类型 默认值 作用
deep boolean false 是否深度监听对象内部属性的变化
immediate boolean false 是否在组件初始化时立即执行回调

deep: true 的行为

watch: {
  userInfo: {
    handler(newVal) {
      console.log('userInfo 变化了:', newVal)
    },
    deep: true  // 不设置 deep,修改 userInfo.name 不会触发回调
  }
}

deep: true 时,userInfo.nameuserInfo.age 的任何变化都会触发 handler 函数。

2.4 $watch API(程序式监听)

Vue 2 提供了 $watch 实例方法,可以在任何地方动态创建监听器:

export default {
  mounted() {
    // 创建监听器
    const unwatch = this.$watch(
      'count',
      (newVal, oldVal) => {
        console.log(`count 变化: ${oldVal} -> ${newVal}`)
      },
      { immediate: true, deep: false }
    )
    
    // 在适当的时机取消监听
    // unwatch()
  },
  beforeDestroy() {
    // 组件销毁前取消监听,防止内存泄漏
    if (this.unwatch) {
      this.unwatch()
    }
  }
}

⚠️ 注意$watch 返回一个取消监听函数,在组件销毁前应主动调用,避免内存泄漏。

三、Vue 3 中的 watch(组合式 API)

Vue 3 的 watch 发生了重大变革——从选项对象变成了独立的函数,可以导入并在 setup() 中使用。

3.1 基本用法

Vue 3 的 watch 函数接收三个参数

watch(source, callback, options)
参数 类型 说明
source ref / reactive / getter / 数组 要监听的数据源
callback Function 数据变化时执行的回调函数
options Object 可选配置项(deepimmediateflush 等)

3.2 监听不同类型的数据源

监听 ref

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

const count = ref(0)
const message = ref('Hello')

// 监听单个 ref
watch(count, (newVal, oldVal) => {
  console.log(`count 变化: ${oldVal} -> ${newVal}`)
})

// 监听 ref 时,直接传入 ref 变量即可,不需要 .value
watch(message, (newVal, oldVal) => {
  console.log(`消息变化: ${oldVal} -> ${newVal}`)
})
</script>

监听 reactive 对象

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

const user = reactive({
  name: '张三',
  age: 25,
  address: {
    city: '深圳',
    district: '南山区'
  }
})

// 直接监听 reactive 对象——默认开启 deep(Vue 3 的特性)
watch(user, (newVal, oldVal) => {
  console.log('user 变化了:', newVal)
})

// 监听 reactive 对象的某个具体属性——需要使用 getter 函数
watch(
  () => user.name,
  (newVal, oldVal) => {
    console.log(`姓名变化: ${oldVal} -> ${newVal}`)
  }
)
</script>

⚠️ 重要:监听 reactive 对象时,默认启用深度监听(与 Vue 2 不同)。但 oldValuenewValue指向同一个对象,无法获取变化前的完整快照。

监听 getter 函数

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

const count = ref(0)
const user = reactive({ name: '张三' })

// 使用 getter 函数返回需要监听的值
watch(
  () => count.value + user.name.length,
  (newVal, oldVal) => {
    console.log(`总和变化: ${oldVal} -> ${newVal}`)
  }
)
</script>

3.3 监听多个数据源

Vue 3 支持在一个 watch 中同时监听多个数据源

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

const count = ref(0)
const user = reactive({ name: '张三', age: 25 })

// 监听多个源——传入数组
watch(
  [count, () => user.name, () => user.age],
  ([newCount, newName, newAge], [oldCount, oldName, oldAge]) => {
    console.log(`count: ${oldCount}->${newCount}`)
    console.log(`姓名: ${oldName}->${newName}`)
    console.log(`年龄: ${oldAge}->${newAge}`)
  }
)
</script>

3.4 选项详解(Vue 3 新增)

Vue 3 的 watch 在 Vue 2 的 deepimmediate 基础上,新增了 flush 选项:

选项 类型 默认值 作用
deep boolean false(ref)/ true(reactive) 深度监听
immediate boolean false 立即执行回调
flush 'pre' / 'post' / 'sync' 'pre' 控制回调执行时机
onTrack Function 调试:依赖被追踪时触发
onTrigger Function 调试:依赖变化触发更新时触发

flush 选项详解

// DOM 更新前执行(默认)
watch(source, callback, { flush: 'pre' })

// DOM 更新后执行——适合需要操作 DOM 的场景
watch(source, callback, { flush: 'post' })

// 同步执行——数据变化时立即执行,性能开销较大
watch(source, callback, { flush: 'sync' })

3.5 停止监听

watch 函数会返回一个停止监听的函数

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

const count = ref(0)

// watch 返回停止函数
const stopWatch = watch(count, (newVal) => {
  console.log('count:', newVal)
})

// 在适当的时机停止监听
const handleStop = () => {
  stopWatch()
}

// 组件卸载时会自动停止,一般不需要手动调用
</script>

3.6 watchEffect:自动追踪依赖

Vue 3 还提供了 watchEffect,与 watch 不同的是,它会自动收集依赖,并在任何依赖变化时重新执行:

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

const count = ref(0)
const user = reactive({ name: '张三' })

// watchEffect 自动追踪内部用到的所有响应式数据
watchEffect(() => {
  console.log(`count 是 ${count.value},用户名是 ${user.name}`)
  // 只要 count 或 user.name 变化,这个函数就会重新执行
})

// watchEffect 也返回停止函数
const stop = watchEffect(() => {
  // 副作用逻辑
})

// 停止监听
// stop()
</script>

watch vs watchEffect 对比

对比维度 watch watchEffect
依赖声明 显式指定数据源 自动收集依赖
执行时机 惰性:依赖变化时才执行 立即执行,首次加载就执行
获取旧值 ✅ 可获取 (newVal, oldVal) ❌ 只能获取当前值
适用场景 需要精确控制监听源和旧值 多个依赖联动,不需要旧值

四、Vue 2 与 Vue 3 选项式 API 的 watch 对比

除了组合式 API 的 watch 函数,Vue 3 仍然支持选项式 API 中的 watch 选项:

Vue 2

export default {
  data() {
    return { count: 0 }
  },
  watch: {
    count(newVal, oldVal) {
      console.log(`count: ${oldVal} -> ${newVal}`)
    }
  }
}

Vue 3(选项式 API)

import { watch } from 'vue'

export default {
  data() {
    return { count: 0 }
  },
  watch: {
    count(newVal, oldVal) {
      console.log(`count: ${oldVal} -> ${newVal}`)
    }
  },
  created() {
    // 也可以在 created 中使用 watch 函数
    watch(
      () => this.count,
      (newVal, oldVal) => {
        console.log(`count: ${oldVal} -> ${newVal}`)
      }
    )
  }
}

五、底层原理对比

5.1 Vue 2 的原理:基于 Object.defineProperty

Vue 2 的响应式系统基于 Object.defineProperty

  1. 初始化时拦截属性:递归遍历数据对象的每一个属性,使用 Object.defineProperty 拦截属性的 getter/setter
  2. 为每个监听表达式创建 Watcher 实例
  3. 依赖收集:访问属性时,将当前的 Watcher 记录为该属性的依赖
  4. 触发更新:属性值变化时,通知相关的 Watcher,调用回调函数

Vue 2 的 watch 本质上是对 Watcher 的一种封装

5.2 Vue 3 的原理:基于 Proxy

Vue 3 的响应式系统基于 Proxywatch 基于 effect 副作用机制实现:

  1. 使用 Proxy 拦截:直接拦截对数据对象的访问和修改,无需递归遍历每个属性
  2. 依赖收集与触发:与 Vue 2 类似,但 Proxy 可以拦截更多操作(新增属性、删除属性、数组索引修改等)
  3. watch 内部封装 effectwatch 在内部封装了 effect 副作用函数,并配有调度器(scheduler)
  4. 调度执行:当数据被读取时进行依赖追踪(track);当数据被修改时,通过调度器执行关联的回调函数

5.3 核心差异总结

对比维度 Vue 2 Vue 3
底层实现 Object.defineProperty Proxy
Watcher 机制 每个监听创建一个 Watcher 实例 基于 effect 副作用系统
新增/删除属性 无法自动检测 ✅ 自动检测
数组索引修改 无法自动检测 ✅ 自动检测
性能 初始化遍历所有属性 惰性代理,更高效
深度监听默认行为 deep: false ref:false / reactive:true

六、真实项目实战

6.1 场景一:搜索框实时搜索(防抖)

这是 watch 最经典的用法——监听搜索关键词变化,发起 API 请求。

Vue 2 实现

<template>
  <div>
    <input v-model="searchKeyword" placeholder="请输入搜索关键词" />
    <div v-if="loading">加载中...</div>
    <ul>
      <li v-for="item in searchResults" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
import { debounce } from 'lodash-es'

export default {
  data() {
    return {
      searchKeyword: '',
      searchResults: [],
      loading: false
    }
  },
  watch: {
    searchKeyword: {
      handler(newVal) {
        if (newVal.trim()) {
          this.debouncedSearch(newVal)
        } else {
          this.searchResults = []
        }
      },
      immediate: false
    }
  },
  created() {
    // 创建防抖函数
    this.debouncedSearch = debounce(this.fetchSearchResults, 500)
  },
  methods: {
    async fetchSearchResults(keyword) {
      this.loading = true
      try {
        const res = await api.search(keyword)
        this.searchResults = res.data
      } finally {
        this.loading = false
      }
    }
  },
  beforeDestroy() {
    // 取消防抖,防止内存泄漏
    this.debouncedSearch.cancel()
  }
}
</script>

Vue 3 实现(组合式 API)

<template>
  <div>
    <input v-model="searchKeyword" placeholder="请输入搜索关键词" />
    <div v-if="loading">加载中...</div>
    <ul>
      <li v-for="item in searchResults" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'
import { debounce } from 'lodash-es'

const searchKeyword = ref('')
const searchResults = ref([])
const loading = ref(false)

// 封装搜索逻辑
const fetchSearchResults = async (keyword) => {
  if (!keyword.trim()) {
    searchResults.value = []
    return
  }
  loading.value = true
  try {
    const res = await api.search(keyword)
    searchResults.value = res.data
  } finally {
    loading.value = false
  }
}

// 创建防抖函数
const debouncedSearch = debounce(fetchSearchResults, 500)

// 监听搜索关键词变化
watch(searchKeyword, (newVal) => {
  debouncedSearch(newVal)
})

// 组件卸载时取消防抖
import { onBeforeUnmount } from 'vue'
onBeforeUnmount(() => {
  debouncedSearch.cancel()
})
</script>

6.2 场景二:表单联动——省市区级联

监听省份变化,自动加载对应的城市列表。

Vue 3 实现

<template>
  <div>
    <select v-model="selectedProvince">
      <option v-for="p in provinces" :key="p.id" :value="p.id">
        {{ p.name }}
      </option>
    </select>
    <select v-model="selectedCity">
      <option v-for="c in cities" :key="c.id" :value="c.id">
        {{ c.name }}
      </option>
    </select>
  </div>
</template>

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

const selectedProvince = ref('')
const selectedCity = ref('')
const cities = ref([])

const provinces = reactive([
  { id: 'gd', name: '广东省' },
  { id: 'zj', name: '浙江省' },
  { id: 'js', name: '江苏省' }
])

// 模拟城市数据
const cityMap = {
  'gd': ['深圳市', '广州市', '东莞市'],
  'zj': ['杭州市', '宁波市', '温州市'],
  'js': ['南京市', '苏州市', '无锡市']
}

// 监听省份变化,加载城市列表
watch(selectedProvince, async (newProvince) => {
  if (!newProvince) {
    cities.value = []
    selectedCity.value = ''
    return
  }
  
  // 模拟异步加载
  const cityList = cityMap[newProvince] || []
  cities.value = cityList.map(name => ({ id: name, name }))
  selectedCity.value = ''
})

// 监听城市变化,执行后续逻辑(如加载区县)
watch(selectedCity, (newCity) => {
  if (newCity) {
    console.log('选中的城市:', newCity)
    // 加载区县列表或执行其他逻辑
  }
})
</script>

6.3 场景三:深度监听表单自动保存

监听复杂表单对象的任何变化,自动保存到 localStorage。

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

const form = reactive({
  title: '',
  content: '',
  tags: [],
  settings: {
    isPublic: true,
    allowComments: true
  }
})

// 从 localStorage 恢复数据
onMounted(() => {
  const saved = localStorage.getItem('draft')
  if (saved) {
    Object.assign(form, JSON.parse(saved))
  }
})

// 深度监听表单变化,自动保存草稿
watch(
  form,
  (newVal) => {
    localStorage.setItem('draft', JSON.stringify(newVal))
    console.log('草稿已自动保存')
  },
  { deep: true }  // 深度监听所有嵌套属性
)
</script>

6.4 场景四:监听路由变化(Vue Router)

在 Vue 3 + Vue Router 中,监听路由参数变化以重新加载页面数据。

<script setup>
import { watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()

// 监听路由参数变化
watch(
  () => route.params.id,
  (newId, oldId) => {
    if (newId !== oldId) {
      console.log(`路由 ID 从 ${oldId} 变为 ${newId}`)
      fetchData(newId)
    }
  },
  { immediate: true }  // 首次加载也执行
)

const fetchData = async (id) => {
  // 根据 ID 加载数据
}
</script>

七、注意事项与最佳实践

7.1 ⚠️ 监听 reactive 对象时 oldValue 的问题

在 Vue 3 中,监听 reactive 对象时,oldValuenewValue 指向同一个对象,无法获取变化前的快照:

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

// ❌ oldValue 和 newValue 是同一个对象
watch(user, (newVal, oldVal) => {
  console.log(newVal === oldVal) // true
  // oldVal 已经被修改,无法获取变化前的值
})

// ✅ 解决方案:使用 getter 监听具体属性
watch(
  () => ({ ...user }),  // 创建副本
  (newVal, oldVal) => {
    // 现在 newVal 和 oldVal 是不同的对象
  }
)

7.2 ⚠️ watch 函数应在 setup 中同步注册

setup() 函数中,watch 应该同步注册

// ❌ 错误:在异步操作后注册 watch
async setup() {
  const data = await fetchData()
  watch(data, () => {})  // 可能不会按预期工作
}

// ✅ 正确:同步注册
setup() {
  const data = ref(null)
  watch(data, () => {})
  fetchData().then(res => data.value = res)
}

7.3 ⚠️ 避免在 watch 中无限循环

watch 回调中修改被监听的数据,可能导致无限循环

// ❌ 危险:无限循环
const count = ref(0)
watch(count, (newVal) => {
  count.value = newVal + 1  // 每次变化又触发新的变化
})

// ✅ 安全:使用条件判断
watch(count, (newVal) => {
  if (newVal < 10) {
    count.value = newVal + 1
  }
})

7.4 ⚠️ 深度监听的性能开销

deep: true 会递归遍历对象的所有属性,对于大型嵌套对象可能带来性能问题。建议:

  • 优先监听具体属性而非整个对象
  • 如果必须深度监听,考虑使用 watchEffect 自动追踪精确依赖
  • 对于超大对象,考虑按需监听或使用 shallowRef / shallowReactive

7.5 ✅ 及时清理副作用

使用 watch 返回的停止函数清理监听,以及使用 onCleanup 清理异步请求:

import { watch, onBeforeUnmount } from 'vue'

const stopWatch = watch(source, (newVal, oldVal, onCleanup) => {
  let cancelled = false
  
  // 注册清理回调
  onCleanup(() => {
    cancelled = true
    console.log('清理副作用')
  })
  
  // 发起异步请求
  fetchData(newVal).then(res => {
    if (!cancelled) {
      // 更新数据
    }
  })
})

// 组件卸载时停止监听
onBeforeUnmount(() => {
  stopWatch()
})

八、总结

Vue 2 vs Vue 3 watch 全景对比

对比维度 Vue 2(Options API) Vue 3(组合式 API)
定义方式 watch 选项对象 watch() 函数
数据源类型 字符串路径(如 'user.name' ref / reactive / getter / 数组
监听多个源 分别配置多个 watch 一个 watch 监听数组
深度监听默认 deep: false ref:false / reactive:true
停止监听 $watch 返回取消函数 watch() 返回停止函数
执行时机控制 immediate 新增 flush 选项(pre/post/sync
底层实现 Object.defineProperty + Watcher Proxy + effect 副作用系统
watchEffect ❌ 无 ✅ 自动追踪依赖

从 Vue 2 到 Vue 3,watch 的演进体现了 Vue 整体设计哲学的转变:从框架主导的 Options API,走向开发者主导的组合式 APIwatch 函数让逻辑组织更加灵活,watchEffect 让依赖追踪更加智能。

在实际项目中:

  1. Vue 2 项目:继续使用 Options API 的 watch 选项,或使用 $watch 进行程序式监听
  2. Vue 3 新项目:优先使用组合式 API 的 watch 函数,配合 <script setup> 获得最佳开发体验
  3. 需要自动追踪多个依赖:使用 watchEffect,代码更简洁
  4. 需要精确控制监听源和获取旧值:使用 watch

理解 watch 的演进和正确用法,能让你在处理异步逻辑、数据联动、状态持久化等场景时更加得心应手。

posted @ 2026-07-10 23:37  我有满天星辰  阅读(1)  评论(0)    收藏  举报