Code前端首页关于Code前端联系我们

Vue3中的watchEffect到底怎么用?和watch有啥区别?

terry 2天前 阅读数 863 #Vue
文章标签 watchEffectwatch

很多刚上手Vue3组合式API的开发者,都会搞混watch和watchEffect的用法,甚至觉得watchEffect看起来很鸡肋,明明watch就能实现所有功能,为啥还要整个新API?其实搞懂它的设计逻辑和适用场景后,你会发现在很多场景下用watchEffect能省不少冗余代码,还能避免漏加监听源的bug。

先搞懂watchEffect的核心定位是什么

watchEffect的核心特点可以用8个字概括:自动追踪,立即执行,和需要手动指定监听源的watch不同,watchEffect不需要你明确告诉它要监听哪个变量,它会自动扫描你传入的副作用函数里用到的所有响应式数据,只要这些数据发生变化,就会重新运行一遍副作用函数,而且它默认会在页面初始化的时候立即执行一次,不需要像watch那样额外加immediate: true配置。

举个最简单的例子,你要在计数器变化的时候打印最新值,用watch的写法是:

const count = ref(0)
watch(count, (newVal) => {
  console.log('count变了', newVal)
}, { immediate: true })

而用watchEffect的话可以简化成:

const count = ref(0)
watchEffect(() => {
  console.log('count变了', count.value)
})

两段代码的效果完全一致,但watchEffect的写法更简洁,尤其是当你需要监听的变量多的时候,优势会更明显。

watchEffect的基础语法和可配置项说明

watchEffect的完整语法是const stop = watchEffect(effect, options),两个参数和返回值的作用如下: 第一个参数是必填的副作用函数,这个函数里同步访问的所有响应式数据都会被自动收集为依赖,函数还会接收一个onInvalidate参数,用来注册清理逻辑,这个我们后面会单独讲。 第二个参数是可选的配置对象,最常用的是flush属性,用来控制副作用函数的执行时机,它有三个可选值:

  • 默认是pre:会在组件更新前执行,此时访问到的DOM还是更新前的状态,适合做一些数据预处理的操作;
  • 设为post:会在组件更新后执行,如果你的副作用里需要操作更新后的DOM,就用这个配置,Vue也专门提供了语法糖watchPostEffect,就是配置了flush: 'post'的watchEffect;
  • 设为sync:会在依赖变化时同步触发执行,性能消耗比较大,除非有特殊的同步需求否则不建议使用,对应的语法糖是watchSyncEffect

watchEffect的返回值是一个停止监听函数,调用这个函数就可以终止后续的依赖监听,默认情况下,在<script setup>里定义的watchEffect会在组件卸载时自动停止,但如果你是在异步函数(比如setTimeout、接口回调)里创建的watchEffect,就需要手动调用停止函数避免内存泄漏。

watchEffect的副作用清理机制怎么用?

很多新手用watchEffect踩的第一个坑,就是不知道它的副作用清理功能,导致出现重复请求、定时器叠加之类的问题,其实watchEffect的副作用函数接收的onInvalidate参数,就是专门用来处理这类问题的。 onInvalidate是一个函数,你可以传入一个回调来注册清理逻辑,这个回调会在两个时机触发:一是watchEffect的依赖变化,副作用函数即将重新执行之前;二是watchEffect被停止(包括组件卸载自动停止)的时候。 最典型的应用场景就是搜索请求的防抖和取消:用户在搜索框输入关键词时,每次输入变化都会触发新的搜索请求,如果之前的请求还没返回就已经没用了,这时候就可以用onInvalidate取消旧请求,避免旧请求晚于新请求返回导致的结果错乱,代码示例如下:

const searchKw = ref('')
const searchResult = ref([])
watchEffect((onInvalidate) => {
  // 空关键词直接清空结果
  if (!searchKw.value.trim()) {
    searchResult.value = []
    return
  }
  // 创建请求中断实例
  const controller = new AbortController()
  fetch(`/api/search?kw=${encodeURIComponent(searchKw.value)}`, {
    signal: controller.signal
  })
  .then(res => res.json())
  .then(data => {
    searchResult.value = data.list
  })
  .catch(err => {
    // 主动中断的请求不用报错
    if (err.name !== 'AbortError') {
      console.error('搜索失败', err)
    }
  })
  // 注册清理逻辑:依赖变化或停止监听时中断请求
  onInvalidate(() => {
    controller.abort()
  })
})

除了取消请求之外,清理定时器、取消事件监听这类操作都可以放在onInvalidate里,能避免很多内存泄漏问题。

实际开发中watchEffect和watch怎么选?

很多人纠结这两个API的使用场景,其实只要记住3个判断标准就行:

  1. 如果你需要拿到变量变化前后的新旧值做对比,或者只需要监听特定的少数几个变量,优先用watch,代码逻辑更清晰,可读性更高;
  2. 如果你需要监听的变量比较多,或者不需要知道具体哪个变量变了,只要相关依赖变化就执行逻辑,优先用watchEffect,不用手动维护监听源列表,后面加新依赖的时候直接在副作用里用就行,不容易出现漏加监听源的bug;
  3. 如果逻辑需要页面初始化就执行一次,用watchEffect可以省掉immediate配置,比watch更简洁。 举个实际场景:你需要把用户填写的姓名、手机号、邮箱三个字段同步存到本地存储,用watch的话需要写watch([name, phone, email], () => { save() }, {immediate: true}),后续如果加了地址字段还要改监听源数组;用watchEffect的话直接写watchEffect(() => { localStorage.setItem('userInfo', JSON.stringify({name: name.value, phone: phone.value, email: email.value})) })就行,后续加字段直接在里面补就行,更方便。

使用watchEffect最容易踩的几个坑

  1. 异步回调里的响应式数据不会被追踪:Vue的依赖收集是同步执行的,如果你把响应式数据的访问放到setTimeout、接口回调这类异步逻辑里,watchEffect执行的时候异步逻辑还没跑,就收集不到对应的依赖,后续这个数据变化也不会触发副作用重新执行,正确的做法是把响应式数据的访问放到副作用函数的同步顶层,const num = count

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门