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

watch是干啥的?怎么用?

terry 10小时前 阅读数 7 #Vue

咱做Vue开发时,肯定绕不开watch和computed这俩工具,尤其是刚接触Vue3组合式API的同学,经常懵:这俩都能响应数据变化,到底有啥不一样?啥场景用哪个?今天咱就掰开揉碎了聊,从“是干啥的”“咋用”到“区别”“场景选择”,一次讲透~

watch的核心作用是监听响应式数据变化,触发“副作用”逻辑。“副作用”指发请求、修改DOM、打印日志这类不直接返回值,却要做额外操作的事儿,下面从“监听的源”“回调逻辑”“配置选项”三部分拆解用法:

监听的“源”可以是啥?

Vue3里watch能监听的源很灵活:

  • 单个ref/reactive变量:若监听ref定义的数值(如const count = ref(0)),直接传watch(count, (newVal) => { ... });若监听reactive对象的属性,得用“getter函数”,比如const state = reactive({ name: '张三' }),要写成watch(() => state.name, (newName) => { ... })——这样只监听name变化,性能更优,不用对整个state开深度监听。
  • 多个源组成的数组:若需同时监听countname,传数组watch([count, () => state.name], ([newCount, newName]) => { ... }),回调里新值也呈数组形式。
  • 复杂getter函数:甚至能监听“派生后的数据”,比如watch(() => state.list.length, (newLen) => { ... }),专门盯数组长度变化。

回调函数里能干啥?

回调接收newVal(变化后的值)和oldVal(变化前的值)两个参数,但要注意:若监听reactive对象,oldValnewVal可能指向同一引用(因reactive是代理对象,修改内部属性不会换引用),这时候得自己判断变化的字段。

举个实际例子:用户输入搜索关键词,每次变化后发请求获取列表,代码如下:

const searchKey = ref('')
watch(searchKey, (newKey) => {
  // 发请求拿数据,这是典型“副作用”
  fetchList(newKey).then(res => {
    list.value = res.data
  })
})

配置选项里的门道

watch第三个参数是配置对象,常用选项有这些:

  • immediate: true:组件一加载,立刻执行一次回调,比如页面加载时,需根据默认searchKey先请求数据,就加这个。
  • deep: true:对reactive对象做“深度监听”,比如state是嵌套对象{ user: { age: 18 } },若直接传state当源,得开deep才能监听到user.age变化,但注意!deep会遍历对象所有属性,数据复杂时性能拉胯,所以优先用getter函数监听具体属性(如() => state.user.age),别依赖deep
  • flush: 'pre' | 'post' | 'sync':控制回调执行时机,默认pre(组件更新前执行),post是组件更新后(适合操作DOM,因这时DOM已更新),sync是数据一变立刻执行(少用,性能风险高)。

举个deep的反面例子:若state是多层嵌套的大对象,开deep监听整个state,每次哪怕改个小属性,Vue都得递归遍历所有子属性,性能肯定崩,所以尽量精准监听,用getter函数只盯需要的字段。

computed又是干啥的?用法有啥特点?

computed叫“计算属性”,核心是基于已有响应式数据,派生新的响应式数据,且自动缓存结果,说白点,把多个数据的计算逻辑封装成新变量,仅在依赖变化时重新计算”。

为啥要用computed?和methods有啥区别?

先对比methods:比如页面要显示“全名”,由firstNamelastName拼接,若用methods,写个fullName()方法,每次组件渲染都会执行;但用computed,定义const fullName = computed(() => firstName.value + lastName.value),只有firstNamelastName变化时,才会重新计算,否则直接拿缓存结果。

所以computed的缓存机制是关键:避免重复计算,提升性能,尤其计算逻辑复杂(如遍历大数组、多次运算)时,缓存能省很多性能。

computed的两种写法

  • 函数式(只读):最常用,比如上面的fullName,直接返回计算后的值:
    const fullName = computed(() => {
      return firstName.value + ' ' + lastName.value
    })
  • 对象式(可写,带get/set):适合“双向绑定”场景,比如用户编辑个人信息,页面上v-model绑定到computed,这时需要setter来更新源数据:
    const fullName = computed({
      get() {
        return user.firstName + ' ' + user.lastName
      },
      set(newVal) {
        // 拆分新值,更新源数据
        const [first, last] = newVal.split(' ')
        user.firstName = first
        user.lastName = last
      }
    })

    页面里用v-model="fullName",输入新全名时,setter会自动触发,更新userfirstNamelastName

computed的“惰性”和“缓存”

computed是惰性计算的:只有“访问”这个计算属性时,才会执行计算逻辑,而且只要依赖的响应式数据没变化,每次访问拿到的都是缓存结果。

举个例子:购物车计算总价,依赖商品列表goodsList和每个商品的price,只要goodsListprice没变化,不管页面渲染多少次,totalPrice的计算逻辑只执行一次,之后直接拿缓存,代码:

const totalPrice = computed(() => {
  console.log('计算总价~') // 只在依赖变化时打印
  return goodsList.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})

若用methods写calcTotalPrice(),每次渲染都得执行reduce,数据多的时候就卡了。

watch和computed核心区别在哪?

功能本质、依赖处理、执行时机、缓存机制几个维度对比,区别一目了然:

维度 watch computed
功能本质 监听变化,执行“副作用”(异步、复杂逻辑) 派生新的响应式数据(同步、纯计算)
依赖处理 可监听多个源(ref/reactive/getter) 自动收集依赖(仅基于响应式数据)
执行时机 数据变化触发回调 依赖变化时惰性计算(访问时执行)
缓存机制 无缓存(每次变化都执行回调) 有缓存(依赖不变则复用结果)
副作用允许 允许(发请求、改DOM等) 不允许(必须纯函数,不能有副作用)

举个直观例子:用户输入用户名,实时验证是否已存在。

  • 用watch:监听username变化,发请求验证(异步副作用),把结果存到isValid里。
  • 用computed:若验证是同步逻辑(如正则匹配长度),可用computed返回是否合法;但若是异步请求(必须等接口返回),computed就不行——因computed必须同步返回值,这时候只能用watch。

啥时候用watch?啥时候用computed?

结合实际场景选工具,才是效率开发的关键:

用watch的场景:

  1. 需要处理异步逻辑:如数据变化后发请求、操作定时器,像搜索关键词变化后发请求(前面的例子),或用户登录状态变化后,异步加载权限菜单。
  2. 需要监听多个数据变化,组合逻辑:如购物车数量变化+商品单价变化,同时满足时才执行某逻辑,用watch监听这两个源,在回调里判断是否触发。
  3. 需要对变化做复杂处理:如数据变化后修改DOM(得用flush: 'post'确保DOM已更新)、记录操作日志、修改多个响应式数据的联动逻辑。
  4. 需要精细控制执行时机:如用flush: 'post'在DOM更新后操作DOM,避免拿到旧DOM状态;或用immediate让组件加载时先执行一次逻辑。

举个场景:用户选择城市后,异步加载该城市的景点列表,同时记录选择日志,代码:

const city = ref('北京')
watch(city, async (newCity) => {
  // 异步请求
  const spots = await fetchSpots(newCity)
  spotList.value = spots
  // 记录日志(副作用)
  logUserAction(`选择城市:${newCity}`)
}, { immediate: true }) // 页面加载时先请求北京的景点

用computed的场景:

  1. 需要派生一个新的响应式数据:如全名(firstName+lastName)、购物车总价、过滤后的列表(如搜索时,根据关键词过滤商品列表)。
  2. 需要缓存计算结果,避免重复计算:如遍历大数组做筛选、多次数学运算,用computed缓存结果,减少性能消耗。
  3. 需要定义“可写”的计算属性:如表单的双向绑定场景,用computed的setter同步更新多个源数据(前面的fullName例子)。

举个过滤列表的例子:页面有搜索框,实时过滤商品列表,用computed缓存过滤结果,避免每次输入都重新遍历整个列表:

const searchKey = ref('')
const goodsList = ref([...]) // 原始商品列表
const filteredList = computed(() => {
  return goodsList.value.filter(item => {
    return item.name.includes(searchKey.value)
  })
})

这样每次searchKey变化时,filteredList才会重新计算,否则直接用缓存,性能比每次调用methods里的过滤函数好太多。

容易踩的坑和最佳实践

最后补点避坑指南,让大家用得更顺:

watch的坑:

  • 监听reactive对象时,别直接传对象+开deep!性能爆炸,优先用getter函数监听具体属性,比如watch(() => state.user.age, ...),而非watch(state, ..., { deep: true })
  • 回调里若修改响应式数据,注意循环触发,比如watch监听count,回调里又改count,会无限循环,得加判断条件,如if (newVal !== oldVal)再执行。

computed的坑:

  • 不能有副作用!如在computed里发请求、修改其他响应式数据、操作DOM,都是错误的,因computed是“计算值”,设计上是纯函数,有副作用会导致逻辑混乱,还可能触发无限更新。
  • 别把computed当methods用,若计算逻辑不需要缓存(如每次调用都要 fresh 结果),就用methods,比如点击按钮时执行的临时计算,用methods更合适。

watch和computed都是Vue3响应式系统的核心,但分工明确:watch负责“监听变化做动作”,适合异步、复杂逻辑;computed负责“派生新数据+缓存”,适合同步、纯计算,记住它们的区别和场景,写代码时就不会纠结选哪个啦~要是还有细节没搞懂,评论区随时聊~

版权声明

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

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门