Vue3 watch 与 watchEffect 完全解析(建议收藏)【vue3】

目录

引言

相比大家刚接触vue3的时候都有一个疑问:**既然都有监听作用,那为什么 Vue 要提供两个 API?什么时候该用 watch,什么时候该用 watchEffect?**实际上,他们设计的目的完全不同。

一句话总结:

  • watch:我指定监听谁。
  • watchEffect:Vue 自动帮我收集依赖。

下面从原理、使用方式、执行时机、性能、适用场景等方面详细讲解。

什么是副作用(Effect)?

解释:

vue官网经常提到一个词:Effect(副作用),什么叫副作用? 不是修改响应式数据本身,而是因为数据变化,需要额外执行一些操作。

例如:

javascript 复制代码
const count = ref(0)

// count变化后发送请求
watch(count, () => {
  fetchData()
})

这里

javascript 复制代码
count变化
    ↓
发送请求

发送请求就是副作用。

常见的副作用包括:

  • 请求接口
  • 修改DOM
  • LocalStorage
  • SessionStorage
  • console.log
  • 开启定时器
  • 销毁定时器
  • 第三方库初始化

watch的工作原理

watch是"明确监听"

watch基本用法

javascript 复制代码
const count = ref(0)

watch(count, () => {
  console.log('count变化')
})

流程:

javascript 复制代码
count
 │
 │
 ▼
watch
 │
 ▼
callback

Vue 不会去分析 callback,他只知道,我监听count。 至于callback里面写的是什么,他不关心。

watch可以监听多个数据

javascript 复制代码
watch([count, name], () => {
  console.log('任意一个变化')
})

流程:

javascript 复制代码
count ─┐
        │
name ───┤
        ▼
     watch

watch可以监听getter

javascript 复制代码
watch(
  () => user.age,
  () => {}
)

注意这里监听的是user.age,而不是user

所以如果:

javascript 复制代码
user.name = 'Tom'

是不会触发的。

watchEffect 的工作原理

watchEffect 不需要指定监听对象。

例如:

javascript 复制代码
const count = ref(0)

watchEffect(() => {
  console.log(count.value)
})

这里没有写watch(count),vue怎么知道监听count?

答案就是:运行一次 callback

第一次执行:

javascript 复制代码
watchEffect(() => {
    console.log(count.value)
})

执行过程中,vue发现读取了 count.value,于是自动记录:

javascript 复制代码
Effect
   │
   ▼
count

以后:

javascript 复制代码
count变化
      ↓
重新执行 Effect

所以:watchEffect 的本质就是:执行一次函数,自动收集里面访问过的所有响应式数据

依赖收集

举个例子:

javascript 复制代码
const count = ref(0)
const name = ref('Tom')

watchEffect(() => {
    console.log(count.value)
})

这里只读取了count.value

vue建立了:

javascript 复制代码
count
   │
   ▼
effect

所以count.value++就会触发,在这例子里面name.value = 'Jack'不会触发,因为根本没有读取。

如果:

javascript 复制代码
watchEffect(() => {
    console.log(count.value)
    console.log(name.value)
})

依赖就会变成:

javascript 复制代码
count ─┐
        │
name ───┤
        ▼
     effect

这样任意一个变化都会执行了。

最大的区别

watch需要自己指定:

javascript 复制代码
watch(source, callback)

watchEffect不用指定,Vue 自动分析:

javascript 复制代码
watchEffect(() => {
    console.log(count.value)
})

watch = 指定依赖;watchEffect = 自动依赖

执行时机区别

watch 默认不会立即执行

例如:

javascript 复制代码
const count = ref(0)

watch(count, () => {
    console.log('执行')
})

页面初始化:不会执行,只有count.value++的时候才会执行。

如果需要第一次执行:

javascript 复制代码
watch(
    count,
    () => {},
    {
        immediate: true
    }
)

流程:

javascript 复制代码
创建watch
      ↓
立即执行一次
      ↓
以后变化继续执行

watchEffect 默认立即执行

例如:

javascript 复制代码
watchEffect(() => {
    console.log(count.value)
})

页面刚创建,立即执行:count = 0,以后变化的时候继续执行,所以他是相当于:

javascript 复制代码
watch(
    count,
    callback,
    {
        immediate: true
    }
)

但并不完全等价。

能拿到旧值吗?

watch可以

javascript 复制代码
watch(count, (newValue, oldValue) => {
    console.log(newValue)
    console.log(oldValue)
})

输出:

javascript 复制代码
1 0
2 1
3 2

watchEffect不可以

javascript 复制代码
watchEffect(() => {})

只有重新执行,没有oldValue;因为:它不是监听某个变量,它只是重新执行整个 Effect。

可以监听多个来源吗?

watch可以

javascript 复制代码
watch(
    [count, name],
    ([newCount, newName], [oldCount, oldName]) => {

    }
)

watchEffect不用 ,因为他会自动收集。

例如:

javascript 复制代码
watchEffect(() => {
    console.log(count.value)
    console.log(name.value)
})

vue就已经知道,监听两个。

性能区别

javascript 复制代码
watchEffect(() => {
    console.log(user.value.name)

    console.log(list.value)

    console.log(config.value)

    console.log(permission.value)

    console.log(menu.value)
})

它读取了五个响应式对象,所以任意一个变化都会重新执行整个函数。

而watch只监听:

javascript 复制代码
watch(
    () => user.value.name,
    () => {}
)

例如如上,只监听user.name,其他的

javascript 复制代码
list
menu
config

变化不会执行。因此:如果 Effect 中读取的数据很多,watchEffect 的重新执行次数可能更多。

cleanup(清理副作用)

watchEffect

javascript 复制代码
watchEffect((onCleanup) => {
    const timer = setInterval(() => {
        console.log('运行')
    }, 1000)

    onCleanup(() => {
        clearInterval(timer)
    })
})

流程:

javascript 复制代码
执行Effect
    ↓
创建timer
    ↓
数据变化
    ↓
执行cleanup
    ↓
重新创建timer

否则每次都会新增:

javascript 复制代码
timer1

timer2

timer3

timer4

watch 同样支持:

javascript 复制代码
watch(source, (newValue, oldValue, onCleanup) => {

})
javascript 复制代码
watch(searchKeyword, async (keyword, _, onCleanup) => {
    const controller = new AbortController()

    onCleanup(() => {
        controller.abort()
    })

    const res = await fetch('/api/search', {
        signal: controller.signal
    })

    // ...
})

这样当关键词快速变化时,可以取消上一次未完成的请求,避免旧请求返回覆盖新结果。

什么时候用 watch?

  • 适合:✅ 明确知道监听谁

特点:

  • 精准
  • 性能更好
  • 能拿旧值
  • 可控制触发时机
  • 更适合业务逻辑

什么时候用 watchEffect?

  • 适合:不知道有哪些依赖。
javascript 复制代码
watchEffect(() => {
    title.value =
        `${user.value.name}-${department.value.name}`
})

以后新增了company.value.name,不用改监听,vue会自动收集。

javascript 复制代码
watchEffect(() => {
    document.title =
        user.value.name
})
javascript 复制代码
watchEffect(() => {
    localStorage.setItem(
        'theme',
        theme.value
    )
})

二者对比总结

对比项 watch watchEffect
是否需要指定监听对象 ✅ 是 ❌ 否,自动收集
是否立即执行 ❌ 默认否(可 immediate: true ✅ 默认立即执行
是否能拿到旧值 ✅ 可以 ❌ 不可以
是否适合精准监听 ✅ 非常适合 ⚠️ 不适合
是否适合依赖较多的场景 ⚠️ 需要手动维护 ✅ 非常适合
是否支持清理副作用 ✅ 支持 ✅ 支持
是否更容易误监听 ❌ 不容易 ⚠️ 容易(函数中读取的所有响应式数据都会成为依赖)
典型用途 请求接口、监听路由、监听 props 同步 DOM、同步浏览器状态、自动推导副作用
javascript 复制代码
                 watch
        ┌─────────────────────┐
        │ 我明确告诉 Vue:     │
        │ "请监听这些数据"      │
        └─────────┬───────────┘
                  │
                  ▼
            数据变化 → 回调执行


              watchEffect
        ┌─────────────────────┐
        │ Vue,你自己去分析    │
        │ 我用了哪些响应式数据  │
        └─────────┬───────────┘
                  │
          自动收集依赖
                  │
                  ▼
        任意依赖变化 → 整个 Effect 重新执行

总结

  • watch:关注"谁变了",适合精确监听和业务逻辑
  • watchEffect:关注"这个副作用依赖了什么",适合自动依赖收集和状态同步