在 Vue 3 中,watch 和 watchEffect 都是用于响应式地执行副作用操作的 API,但它们在用法和功能上有重要区别。下面通过具体示例进行详细解释:
🕵️♂️ 1. watchEffect
- 特点:立即执行传入的函数,并自动追踪函数内使用的响应式依赖
- 使用场景:不需要知道具体哪个值变化,只要依赖变化就执行操作
- 无旧值访问:无法获取变化前的值
            
            
              javascript
              
              
            
          
          import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('Alice')
// 自动追踪函数内使用的响应式依赖
watchEffect(() => {
  console.log(`watchEffect: count=${count.value}, name=${name.value}`)
  // 每当 count 或 name 变化时都会执行
})
// 触发执行:
count.value++ // 输出: "watchEffect: count=1, name=Alice"
name.value = 'Bob' // 输出: "watchEffect: count=1, name=Bob"特性说明:
- 
立即执行:初始化时就会执行一次 
- 
自动依赖收集:函数内用到的响应式变量都会被自动追踪 
- 
清理副作用 : javascriptwatchEffect((onCleanup) => { const timer = setTimeout(() => { console.log('Delayed log') }, 1000) onCleanup(() => clearTimeout(timer)) // 清理上一次的副作用 })
🔍 2. watch
- 特点:需要显式指定监听的数据源,可以访问变化前后的值
- 使用场景:需要知道具体哪个值变化,需要访问旧值,需要惰性执行
- 更精细控制:支持 deep、immediate 等选项
基本用法:
            
            
              javascript
              
              
            
          
          import { ref, watch } from 'vue'
const count = ref(0)
// 监听单个 ref
watch(count, (newVal, oldVal) => {
  console.log(`watch: count从 ${oldVal} → ${newVal}`)
})
count.value++ // 输出: "watch: count从 0 → 1"监听多个源:
            
            
              javascript
              
              
            
          
          const count = ref(0)
const name = ref('Alice')
// 监听多个 ref
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
  console.log(`变化: count=${oldCount}→${newCount}, name=${oldName}→${newName}`)
})
count.value++ // 输出: "变化: count=0→1, name=Alice→Alice"
name.value = 'Bob' // 输出: "变化: count=1→1, name=Alice→Bob"监听响应式对象:
            
            
              javascript
              
              
            
          
          const user = reactive({ 
  name: 'Alice', 
  age: 25,
  address: {
    city: 'Beijing'
  }
})
// 监听整个对象(需要 deep 选项)
watch(
  () => ({ ...user }), // 或使用 toRefs
  (newUser, oldUser) => {
    console.log('用户信息变化', newUser)
  },
  { deep: true }
)
// 监听特定属性
watch(
  () => user.age,
  (newAge, oldAge) => {
    console.log(`年龄变化: ${oldAge} → ${newAge}`)
  }
)
// 触发:
user.age = 26 // 输出: "年龄变化: 25 → 26"
user.address.city = 'Shanghai' // 会触发 deep watch高级选项:
            
            
              javascript
              
              
            
          
          watch(
  count,
  (newVal) => {
    console.log('带选项的watch:', newVal)
  },
  {
    immediate: true, // 立即执行一次
    flush: 'post',   // 在DOM更新后执行
    onTrack(e) {     // 调试钩子
      console.debug('依赖被追踪', e)
    },
    onTrigger(e) {   // 调试钩子
      console.debug('依赖触发变化', e)
    }
  }
)⚖️ 3. 核心区别对比
| 特性 | watchEffect | watch | 
|---|---|---|
| 初始执行 | 立即执行 | 默认不执行(可通过 immediate: true启用) | 
| 依赖收集 | 自动收集函数内所有响应式依赖 | 需要显式指定监听源 | 
| 旧值访问 | ❌ 无法获取旧值 | ✅ 可获取旧值 | 
| 监听多个源 | 自动处理多个依赖 | 需用数组语法 watch([src1, src2], ...) | 
| 深度监听 | 总是深度监听(对象内部变化也会触发) | 需显式设置 { deep: true } | 
| 调试工具 | 不支持 onTrack/onTrigger | 支持调试钩子 | 
| 性能优化 | 适合简单逻辑 | 适合复杂监听场景 | 
| 典型使用场景 | 日志、发送分析事件、DOM操作 | 表单验证、数据同步、复杂状态逻辑 | 
🧪 4. 真实场景示例
场景1:自动保存表单(使用 watchEffect)
            
            
              javascript
              
              
            
          
          const formData = reactive({
  title: '',
  content: '',
  lastSaved: null
})
// 当标题或内容变化时自动保存
watchEffect(async (onCleanup) => {
  if (!formData.title && !formData.content) return
  
  // 防抖处理
  const timer = setTimeout(async () => {
    const response = await saveToAPI(formData)
    formData.lastSaved = new Date()
  }, 1000)
  
  onCleanup(() => clearTimeout(timer))
})场景2:路由参数变化加载数据(使用 watch)
            
            
              javascript
              
              
            
          
          import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const post = ref(null)
// 监听路由参数变化
watch(
  () => route.params.id,
  async (newId) => {
    if (!newId) return
    post.value = await fetchPost(newId)
  },
  { immediate: true } // 初始加载
)场景3:组合使用
            
            
              javascript
              
              
            
          
          const user = reactive({ name: 'Alice', age: 30 })
// 当用户年龄变化时执行特定逻辑
watch(
  () => user.age,
  (newAge) => {
    if (newAge >= 18) {
      console.log('用户已成年')
    }
  }
)
// 自动记录所有用户属性变化
watchEffect(() => {
  console.log('用户状态:', JSON.stringify(user))
})⚠️ 5. 重要注意事项
- 
异步操作: javascript// ✅ 正确:在 watch 中使用异步 watch(data, async (newVal) => { const result = await fetchData(newVal) }) // ❌ 错误:不能直接在 async 函数中使用 watchEffect 的清理 watchEffect(async (onCleanup) => { ... })
- 
停止侦听: javascriptconst stop = watchEffect(() => { ... }) const stopWatch = watch(source, callback) // 在需要时停止 stop() stopWatch()
- 
Ref 解包: javascriptconst obj = reactive({ count: ref(0) }) // ✅ 自动解包 watchEffect(() => console.log(obj.count)) // 直接访问值 // ❌ 错误:监听的是 ref 对象而非值 watch(obj.count, (val) => ...) // ✅ 正确:使用 getter 函数 watch(() => obj.count, (val) => ...)
- 
DOM 更新时机: javascriptwatchEffect(() => { console.log('DOM 状态:', document.getElementById('elem')) }, { flush: 'post' }) // 在 DOM 更新后执行
💎 总结选择建议
| 使用 watchEffect当 | 使用 watch当 | 
|---|---|
| 需要立即执行副作用 | 需要惰性执行(首次不执行) | 
| 依赖多个值且不关心具体哪个变化 | 需要知道具体哪个值变化 | 
| 不需要旧值 | 需要访问旧值进行比较 | 
| 简单操作(如日志、DOM操作) | 复杂操作(如API请求、状态验证) | 
| 对象深度变化监听(无需额外配置) | 需要控制监听深度(避免性能问题) | 
经验法则:
- 优先使用 watchEffect处理简单副作用
- 当需要精确控制或访问旧值时使用 watch
- 对于对象内部变化,watchEffect更简单,但watch更可控