一篇文章让你学透在Vue 3 中watch 和 watchEffect的区别

在 Vue 3 中,watchwatchEffect 都是用于响应式地执行副作用操作的 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"

特性说明

  • 立即执行:初始化时就会执行一次

  • 自动依赖收集:函数内用到的响应式变量都会被自动追踪

  • 清理副作用

    javascript 复制代码
    watchEffect((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. 重要注意事项

  1. 异步操作

    javascript 复制代码
    // ✅ 正确:在 watch 中使用异步
    watch(data, async (newVal) => {
      const result = await fetchData(newVal)
    })
    
    // ❌ 错误:不能直接在 async 函数中使用 watchEffect 的清理
    watchEffect(async (onCleanup) => { ... })
  2. 停止侦听

    javascript 复制代码
    const stop = watchEffect(() => { ... })
    const stopWatch = watch(source, callback)
    
    // 在需要时停止
    stop()
    stopWatch()
  3. Ref 解包

    javascript 复制代码
    const obj = reactive({ count: ref(0) })
    
    // ✅ 自动解包
    watchEffect(() => console.log(obj.count)) // 直接访问值
    
    // ❌ 错误:监听的是 ref 对象而非值
    watch(obj.count, (val) => ...) 
    
    // ✅ 正确:使用 getter 函数
    watch(() => obj.count, (val) => ...)
  4. DOM 更新时机

    javascript 复制代码
    watchEffect(() => {
      console.log('DOM 状态:', document.getElementById('elem'))
    }, { flush: 'post' }) // 在 DOM 更新后执行

💎 总结选择建议

使用 watchEffect 使用 watch
需要立即执行副作用 需要惰性执行(首次不执行)
依赖多个值且不关心具体哪个变化 需要知道具体哪个值变化
不需要旧值 需要访问旧值进行比较
简单操作(如日志、DOM操作) 复杂操作(如API请求、状态验证)
对象深度变化监听(无需额外配置) 需要控制监听深度(避免性能问题)

经验法则

  • 优先使用 watchEffect 处理简单副作用
  • 当需要精确控制或访问旧值时使用 watch
  • 对于对象内部变化,watchEffect 更简单,但 watch 更可控
相关推荐
烛阴5 分钟前
提升Web爬虫效率的秘密武器:Puppeteer选择器全攻略
前端·javascript·爬虫
hao_wujing36 分钟前
Web 连接和跟踪
服务器·前端·javascript
想不到耶39 分钟前
Vue3轮播图组件,当前轮播区域有当前图和左右两边图,两边图各显示一半,支持点击跳转和手动滑动切换
开发语言·前端·javascript
我家媳妇儿萌哒哒2 小时前
el-upload 点击上传按钮前先判断条件满足再弹选择文件框
前端·javascript·vue.js
加油,前进2 小时前
layui和vue父子级页面及操作
javascript·vue.js·layui
天天向上10242 小时前
el-tree按照用户勾选的顺序记录节点
前端·javascript·vue.js
咔咔库奇2 小时前
深入探索 Vue 3 Fragments:从原理到实战的全方位指南
前端·javascript·vue.js
java_强哥2 小时前
uniapp实现聊天中的接发消息自动滚动、消息定位和回到底部
javascript·vue.js·uni-app
要加油哦~3 小时前
vue | vue 插件化机制,全局注册 和 局部注册
前端·javascript·vue.js
猫头虎-前端技术3 小时前
HTML 与 CSS 的布局机制(盒模型、盒子定位、浮动、Flexbox、Grid)问题总结大全
前端·javascript·css·vue.js·react.js·前端框架·html