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

Vue3的watch怎么同时监听多个数据?踩坑和优化点都给你理清楚了

terry 9小时前 阅读数 136 #Vue

很多刚上手Vue3的朋友,可能会遇到这样的场景:比如表单里同时依赖“用户名+密码”才能触发验证,或者购物车的“商品总价=单价×数量”的联动,但不知道怎么高效地写监听逻辑——是写两个独立的watch?还是有更简洁的方式?如果有不同数据类型(ref、reactive嵌套属性甚至computed)要一起听,又该怎么处理?今天咱们就从基础用法到进阶场景,再到常见坑点和优化,把这件事说透。

watch监听多个的三种基础写法

先别慌,Vue3官方早就给咱们留了监听多个源的接口,主要有三种常用的方式,分别对应不同的需求场景,咱们一个个试。

用数组包起来直接传

这是最简单、最直观的方式,不管你要监听的是ref、reactive的顶层属性、reactive的嵌套属性(得写成getter函数)、还是computed计算属性,都可以直接塞到一个数组里作为watch的第一个参数,举个例子,假设咱们做一个学生信息录入的表单,监听学生的“姓名”和“班级”两个ref,触发后更新一下侧边栏的“当前录入者标识”:

import { ref, watch } from 'vue';
export default {
  setup() {
    const studentName = ref('');
    const studentClass = ref('');
    const currentTag = ref('待录入');
    // 数组写法:监听两个ref
    watch([studentName, studentClass], (newVals, oldVals) => {
      // newVals是[新姓名, 新班级]的数组,oldVals对应旧值
      const [newName, newCls] = newVals;
      currentTag.value = newName && newCls ? `${newCls}-${newName}` : '待录入';
    });
    return { studentName, studentClass, currentTag };
  }
}

这里要注意,如果监听的是reactive的嵌套属性,比如student对象里的student.name和student.age,不能直接把student、student.name这样的东西(除非是ref包装过的嵌套对象属性)直接放数组里——因为Vue3的reactive返回的是响应式代理的引用,直接监听student的话会监听整个对象的所有变化,而不是具体的某个属性;直接写student.name的话,那只是普通的字符串/数字,没有响应性,这时候得用getter函数把嵌套属性“包”成一个函数,告诉Vue3“我要追踪这个函数里用到的响应式数据”。

import { reactive, watch } from 'vue';
export default {
  setup() {
    const student = reactive({ name: '', age: 0, address: '北京市朝阳区' });
    // 监听reactive的嵌套属性:用getter数组
    watch(
      [() => student.name, () => student.age],
      ([newName, newAge], [oldName, oldAge]) => {
        console.log(`姓名从${oldName}变到${newName},年龄从${oldAge}变到${newAge}`);
      }
    );
    // 触发一下看看:只有修改name或age才会打印,修改address不会
    setTimeout(() => student.name = '张三', 1000);
    setTimeout(() => student.address = '北京市海淀区', 2000);
    setTimeout(() => student.age = 18, 3000);
    return { student };
  }
}

这种getter包数组的写法,是监听多源数据里最灵活的一种,后面讲优化和进阶的时候也会经常用到。

监听同一个reactive对象的多个属性?直接解构数组getter就行

刚才提到不能直接监听整个reactive对象的引用(除非你想监听所有变化),但如果你的需求是“监听reactive里的a、b、c三个属性,只要这三个变就触发,其他的不管”,除了刚才的getter数组,有没有更方便的?其实本质还是一样的,但可以稍微简化一下解构?不对,是可以直接写多个getter,但写法上可以更统一,不过这里有个误区,我见过有人直接写watch([student.a, student.b], ...),刚才说过这是没用的,因为student.a是普通值,不是响应式源,Vue3追踪不到它的变化。

再举个更直观的购物车例子,监听购物车的商品列表里的每个商品的单价和数量,更新总价?不对,监听列表里的每个属性得用深度监听,那个后面再说,先举个简单的reactive多属性getter监听:

import { reactive, watch } from 'vue';
export default {
  setup() {
    const shoppingCart = reactive({
      itemCount: 0, // 商品总件数
      totalPrice: 0, // 总金额,这里先随便设,后面应该用computed
      couponCode: '', // 优惠券码,不影响总件数
      discount: 1 // 折扣率
    });
    // 只监听itemCount和discount的变化,更新临时显示的“预估优惠后的件数折扣率组合”
    watch(
      [() => shoppingCart.itemCount, () => shoppingCart.discount],
      ([newCount, newDisc]) => {
        console.log(`当前有${newCount}件商品,折扣是${newDisc * 10}折`);
      }
    );
    // 修改couponCode不会触发,修改discount会
    setTimeout(() => shoppingCart.couponCode = 'SAVE10', 1000);
    setTimeout(() => shoppingCart.discount = 0.8, 2000);
    return { shoppingCart };
  }
}

用watchEffect?哦不对,watchEffect是自动追踪,不算显式“指定多个”

可能有人会问:watchEffect不是自动追踪所有用到的响应式数据吗?那如果我在watchEffect里同时用多个数据,算不算监听多个?严格来说不算“显式指定多个要监听的源”,而是“自动追踪依赖源”,watch和watchEffect的核心区别就在于这:watch是「懒执行」(第一次不会触发,除非设置immediate)、「显式指定监听源」、「能拿到新值和旧值」;watchEffect是「立即执行」、「自动追踪依赖」、「拿不到旧值」,不过如果你的需求不需要旧值,也不需要懒执行,用watchEffect确实比写数组watch更方便,比如刚才的预估优惠组合,用watchEffect可以写成:

import { reactive, watchEffect } from 'vue';
export default {
  setup() {
    const shoppingCart = reactive({
      itemCount: 0,
      discount: 1,
      couponCode: ''
    });
    // 自动追踪itemCount和discount,立即执行,修改这两个就会再执行
    watchEffect(() => {
      console.log(`当前有${shoppingCart.itemCount}件商品,折扣是${shoppingCart.discount * 10}折`);
    });
    setTimeout(() => shoppingCart.couponCode = 'SAVE10', 1000);
    setTimeout(() => shoppingCart.discount = 0.8, 2000);
    return { shoppingCart };
  }
}

不过这里要注意watchEffect的自动追踪机制:它只会追踪在同步执行过程中用到的响应式数据,如果你在watchEffect里写了异步代码,比如setTimeout里用到了shoppingCart.otherData,那otherData的变化不会触发watchEffect的重新执行。

watchEffect(() => {
  // 同步部分:只追踪discount
  console.log(`折扣是${shoppingCart.discount * 10}折`);
  setTimeout(() => {
    // 异步部分:不追踪itemCount,修改itemCount不会触发重新执行
    console.log(`当前有${shoppingCart.itemCount}件商品`);
  }, 100);
});

这个点很容易踩坑,后面会专门讲。

进阶场景怎么处理?

刚才的都是基础的字符串、数字等简单数据类型的监听,那如果是数组变化、对象嵌套变化、computed属性和ref/reactive混合监听呢?咱们逐个来看。

监听多个“需要深度对比”的源

比如刚才的购物车商品列表,假设商品列表是个ref数组,或者reactive里的数组属性,每个商品有id、price、count,咱们要监听这个数组里的price或count的变化,更新总价(总价优先用computed,但如果要做一些额外的操作,比如埋点、发送请求统计价格变化,就得用watch了),这时候就需要给数组里的每个getter(或者数组本身的getter)设置deep: true选项。

举个商品列表埋点的例子:

import { reactive, watch } from 'vue';
export default {
  setup() {
    const shoppingCart = reactive({
      items: [
        { id: 1, name: '笔记本电脑', price: 5999, count: 1 },
        { id: 2, name: '无线鼠标', price: 99, count: 2 }
      ],
      couponCode: ''
    });
    // 监听items数组里的所有变化(包括push、pop、splice,以及每个对象的属性变化)
    // 这里用() => shoppingCart.items作为getter,然后设置deep: true
    // 如果要同时监听items和couponCode,就放数组里,deep设置在第三个参数的options里
    watch(
      [() => shoppingCart.items, () => shoppingCart.couponCode],
      ([newItems, newCoupon], [oldItems, oldCoupon]) => {
        // 注意:deep模式下,newItems和oldItems是同一个引用!
        // 因为Vue3不会为了深度对比保存整个旧对象/数组的副本,那样太耗内存
        // 所以这里要判断具体是什么变了,得自己写逻辑,或者用computed算对比后的变化
        console.log('购物车或优惠券发生了变化');
        console.log('新优惠券:', newCoupon);
        console.log('旧优惠券:', oldCoupon); // 优惠券不是数组/对象,能拿到正常的旧值
        console.log('新旧items是否相同:', newItems === oldItems); // 这里会打印true
      },
      { deep: true, immediate: true } // immediate: true 表示第一次加载就执行
    );
    // 测试一下:修改鼠标的count
    setTimeout(() => shoppingCart.items[1].count = 3, 1000);
    // 测试一下:添加商品
    setTimeout(() => shoppingCart.items.push({ id: 3, name: '键盘', price: 199, count: 1 }), 2000);
    // 测试一下:修改优惠券
    setTimeout(() => shoppingCart.couponCode = 'SAVE20', 3000);
    return { shoppingCart };
  }
}

刚才的例子里提到了一个关键点:deep模式下,监听数组/对象的引用时,newVal和oldVal是同一个引用!这是Vue3出于性能考虑做的优化,因为如果要保存整个旧对象/数组的副本,每次深度变化都要深拷贝一次,当数据量很大的时候(比如购物车有1000个商品),内存消耗和计算量都会非常大,所以如果在deep模式下需要对比新旧数组/对象的具体差异,不能直接用newVal === oldVal判断(这个一直是true),也不能直接遍历对比属性(因为引用相同,遍历的是同一个对象),得自己提前用JSON.parse(JSON.stringify())或者第三方库(比如lodash的cloneDeep)保存一下旧值?不对,这样太麻烦,也耗性能,有没有更好的方法?其实可以用computed属性先计算出你关心的“关键值”,然后监听computed属性,这样就不需要deep了,还能拿到正常的新旧值。

比如刚才的购物车,我们关心的是“商品总数量”和“商品总原价”(不包含优惠券)的变化,那可以先写两个computed属性:

import { reactive, computed, watch } from 'vue';
export default {
  setup() {
    const shoppingCart = reactive({
      items: [
        { id: 1, name: '笔记本电脑', price: 5999, count: 1 },
        { id: 2, name: '无线鼠标', price: 99, count: 2 }
      ],
      couponCode: ''
    });
    // 计算总数量
    const totalCount = computed(() => {
      return shoppingCart.items.reduce((sum, item) => sum + item.count, 0);
    });
    // 计算总原价
    const totalOriginalPrice = computed(() => {
      return shoppingCart.items.reduce((sum, item) => sum + item.price * item.count, 0);
    });
    // 监听computed属性和优惠券,不需要deep,还能拿到正常的新旧值!
    watch(
      [totalCount, totalOriginalPrice, () => shoppingCart.couponCode],
      ([newTC, newTOP, newCC], [oldTC, oldTOP, oldCC]) => {
        console.log('关键数据变化了!');
        if (newTC !== oldTC) {
          console.log(`总数量从${oldTC}变到${newTC}`);
          // 这里可以做埋点,商品数量变化”
        }
        if (newTOP !== oldTOP) {
          console.log(`总原价从${oldTOP}变到${newTOP}`);
          // 这里可以做埋点,商品原价变化”
        }
        if (newCC !== oldCC) {
          console.log(`优惠券从${oldCC}变到${newCC}`);
          // 这里可以做埋点,优惠券变更”
        }
      },
      { immediate: true }
    );
    setTimeout(() => shoppingCart.items[1].count = 3, 1000);
    setTimeout(() => shoppingCart.items.push({ id: 3, name: '键盘', price: 199, count: 1 }), 2000);
    setTimeout(() => shoppingCart.couponCode = 'SAVE20', 3000);
    return { shoppingCart, totalCount, totalOriginalPrice };
  }
}

这种“用computed包装关键值替代deep监听”的方法,是Vue3官方文档里推荐的优化方式,既避免了deep监听的性能问题,又能拿到正常的新旧值,还能让代码逻辑更清晰——你监听的不是“整个购物车数组的所有变化”,而是“你真正关心的几个关键指标”。

监听多个源,只要其中一个满足条件就触发?或者要所有都满足条件才触发?

默认情况下,数组watch是“只要其中任意一个监听源发生变化,就会触发回调函数”,那如果我有特殊需求呢?只有当用户名和密码都不为空的时候,才触发登录按钮的激活逻辑”,或者“只有当商品总原价超过1000元,同时优惠券码不为空的时候,才触发优惠券可用的提示”。

对于第一种“只要有一个变就触发,但触发后要判断所有条件”,这个很简单,就是默认的数组watch,然后在回调函数里加if判断就行,比如登录按钮的激活:

import { ref, watch } from 'vue';
export default {
  setup() {
    const username = ref('');
    const password = ref('');
    const isLoginBtnDisabled = ref(true);
    // 默认:只要username或password变就触发
    watch([username, password], ([newU, newP]) => {
      isLoginBtnDisabled.value = !(newU.trim() && newP.trim());
    });
    return { username, password, isLoginBtnDisabled };
  }
}

那如果我想“只有当两个条件同时满足的时候,才触发一次特定的逻辑”?比如刚才的优惠券,只有当总原价第一次超过1000,同时第一次输入了优惠券码,才弹一次提示框,后面再变的话就不弹了(除非总原价掉回1000以下,再超过,同时优惠券码清空再输入,才再弹),这时候可以用watch的第三个参数里的flush选项(不过flush主要控制回调执行的时机),或者在回调函数里加状态变量记录是否已经满足过条件,或者用watch的onCleanup清理函数?不对,onCleanup是用来清理上一次回调的副作用的,比如上一次发送了请求但还没回来,这次又触发了,就取消上一次的请求。

举个加状态变量的例子:

import { reactive, computed, watch } from 'vue';
export default {
  setup() {
    const shoppingCart = reactive({
      items: [
        { id: 1, name: '笔记本', price: 800, count: 1 },
        { id: 2, name: '鼠标', price: 100, count: 1 }
      ],
      couponCode: ''
    });
    const totalOriginalPrice = computed(() => {
      return shoppingCart.items.reduce((sum, item) => sum + item.price * item.count, 0);
    });
    // 状态变量:记录是否已经弹过提示
    let hasShownCouponTip = false;
    // 状态变量:记录上一次是否满足条件
    let lastSatisfied = false;
    watch(
      [totalOriginalPrice, () => shoppingCart.couponCode],
      ([newTOP, newCC]) => {
        const currentSatisfied = newTOP > 1000 && newCC.trim();
        // 只有当“上一次不满足,这一次满足”的时候,才弹提示
        if (!lastSatisfied && currentSatisfied) {
          alert('恭喜您!您的订单满足优惠券使用条件!');
          hasShownCouponTip = true;
        }
        // 更新上一次的状态
        lastSatisfied = currentSatisfied;
      },
      { immediate: true }
    );
    // 测试一下:先总原价刚好900,输入优惠券不弹;再添加一个99的键盘,总原价999,还不弹;再加一个2元的贴纸,总原价1001,弹提示
    setTimeout(() => shoppingCart.couponCode = 'SAVE50', 1000);
    setTimeout(() => shoppingCart.items.push({ id: 3, name: '键盘', price: 99, count: 1 }), 2000);
    setTimeout(() => shoppingCart.items.push({ id: 4, name: '贴纸', price: 2, count: 1 }), 3000);
    // 测试一下:清空优惠券,再输入,总原价还是1001,上一次lastSatisfied是true(清空优惠券后变成false),再输入变成true,会弹吗?
    // 会弹,因为上一次是false,这一次是true
    setTimeout(() => shoppingCart.couponCode = '', 4000);
    setTimeout(() => shoppingCart.couponCode = 'SAVE50', 5000);
    return { shoppingCart, totalOriginalPrice };
  }
}

这个逻辑应该能满足大部分“特定条件触发”的需求了。

监听多个源,回调函数里的新值和旧值对应关系搞不清怎么办?

刚才的例子里,我们都是用数组解构的方式([newU, newP], [oldU, oldP])来获取对应的值,这个对应关系是“和第一个参数数组里的监听源顺序完全一致”的——第一个参数数组的第0个是username,那newVals的第0个就是新username,oldVals的第0个就是旧username;第一个参数数组的第1个是password,那newVals的第1个就是新password,oldVals的第1个就是旧password,以此类推。

所以这里有个小技巧:如果监听源比较多(比如超过3个),可以给第一个参数数组加注释,或者用对象的方式?不对,Vue3的watch第一个参数不支持对象,只支持单个源或者数组源,哦对了,注释是个好方法,

// 监听顺序:用户名、密码、手机号、验证码
watch(
  [
    () => form.username,
    () => form.password,
    () => form.phone,
    () => form.code
  ],
  ([newU, newP, newPh, newCo], [oldU, oldP, oldPh, oldCo]) => {
    // 这里就不会搞混对应关系了
  }
);

常见的5个踩坑点,避坑指南收好!

刚才讲了很多用法,现在咱们来说说新手最容易踩的几个坑,这些坑我身边很多刚学Vue3的朋友都踩过,今天一起整理出来,帮大家避避坑。

坑1:直接监听reactive的嵌套属性(不用getter)

刚才已经提过一次,但这个坑太常见了,必须再强调一遍!

// 错误写法!!!
const form = reactive({ username: '' });
watch(form.username, (newVal) => {
  console.log(newVal); // 永远不会触发,因为form.username是普通字符串
});

正确写法是用getter函数:

// 正确写法!!!
const form = reactive({ username: '' });
watch(() => form.username, (newVal) => {
  console.log(newVal); // 修改form.username会触发
});

坑2:直接监听整个reactive对象,但以为只会监听部分属性

const form = reactive({ username: '', password: '', phone: '' });
watch(form, (newVal) => {
  console.log('表单变化了');
});
// 修改phone也会触发,但你可能只想监听username和password

这时候要么用getter数组只监听username和password,要么用computed包装关键值,要么在回调函数里判断具体是哪个属性变了(不过判断具体属性变了比较麻烦,不如用getter数组)。

坑3:deep模式下以为能拿到正常的新旧数组/对象引用

刚才的购物车例子里也提过,deep模式下,newVal和oldVal是同一个引用!因为Vue3不会深拷贝旧数据,如果要对比新旧差异,要么用computed包装关键值,要么在回调函数里用第三方库(比如lodash的isEqual)对比,但isEqual对比大数组/大对象也耗性能,所以优先推荐用computed包装。

坑4:watchEffect里的异步依赖不会被追踪

刚才也提过,

// 错误写法!!!异步部分的itemCount不会被追踪
watchEffect(() => {
  setTimeout(() => {
    console.log(`当前有${shoppingCart.itemCount}件商品`);
  }, 100);
});

如果异步部分需要用到响应式数据,应该把响应式数据的读取放在watchEffect的同步部分

// 正确写法!!!同步部分读取itemCount,保存到变量里,异步部分用变量
watchEffect(() => {
  const currentCount = shoppingCart.itemCount; // 同步读取,触发追踪
  setTimeout(() => {
    console.log(`当前有${currentCount}件商品`);
  }, 100);
});

不过这样的话,只有在watchEffect执行的那一刻读取到的currentCount会被用到,异步执行的时候如果itemCount又变了,不会更新setTimeout里的console.log,如果要异步执行的时候也用最新的itemCount,那还是得用watch,在回调函数里写异步代码。

坑5:忘记停止watch导致内存泄漏

虽然Vue3会在组件卸载的时候自动停止当前组件内的watch和watchEffect,但如果你是在组件外部创建的watch,或者在异步操作里动态创建的watch,那必须手动停止,否则会导致内存泄漏。

手动停止watch的方法很简单:watch会返回一个停止函数,调用这个函数就可以停止监听了。

import { ref, watch, onUnmounted } from 'vue';
export default {
  setup() {
    const count = ref(0);
    // 保存停止函数
    const stopWatch = watch(count, (newVal) => {
      console.log(newVal);
    });
    // 组件卸载的时候手动停止(不过组件内部的watch其实不需要,但手动加上更保险,尤其是动态创建的)
    onUnmounted(() => {
      stopWatch();
    });
    // 也可以在某个条件满足的时候手动停止,比如count超过10
    watch(count, (newVal) => {
      if (newVal > 10) {
        stopWatch();
        console.log('停止监听count了');
      }
    });
    return { count };
  }
}

优化点总结,让你的代码更高效

刚才讲了避坑,现在咱们再讲几个优化点,让你的watch代码性能更好,逻辑更清晰。

优化1:能用computed就不用watch

这是Vue官方文档里反复强调的!computed是基于依赖缓存的,只有当依赖的响应式数据发生变化时,才会重新计算;而watch是只要监听源发生变化,就会执行回调函数,不管有没有必要,比如计算总价、筛选列表这些纯数据转换的操作,优先用computed;只有当需要做副作用操作的时候(比如发送请求、操作DOM、埋点、设置localStorage),才用watch或watchEffect。

优化2:用computed包装关键值替代deep监听

刚才的购物车例子里已经演示过了,deep监听会递归遍历整个对象/数组,性能消耗很大;而用computed包装关键值,只会在关键值变化的时候触发回调,性能好很多,还能拿到正常的新旧值。

优化3:合理使用immediate选项

immediate选项控制watch是否在第一次加载的时候就执行回调函数,如果你的需求是“第一次加载就要根据初始数据做一些操作”,比如根据初始的用户名和密码设置登录按钮的状态,那可以设置immediate: true,这样就不用在setup里单独写一遍逻辑了。

优化4:合理使用flush选项

flush选项控制watch回调函数的执行时机,有三个可选值:

  1. 'pre'(默认值):在DOM更新之前执行回调函数。
  2. 'post':在DOM更新之后执行回调函数,如果你的回调函数需要操作DOM(比如获取DOM元素的高度、宽度),那必须设置flush: 'post',或者用nextTick。
  3. 'sync':同步执行回调函数,只要监听源发生变化,就立即执行,这个选项性能很差,除非万不得已,否则不要用。

举个flush: 'post'的例子,比如根据列表的长度设置容器的高度:

import { ref, watch, nextTick } from 'vue';
export default {
  setup() {
    const list = ref([1, 2, 3]);
    const containerHeight = ref(0);
    // 方法1:用flush: 'post'
    watch(
      list,
      () => {
        const container = document.getElementById('list-container');
        if (container) {
          containerHeight.value = container.offsetHeight;
        }
      },
      { flush: 'post', deep: true }
    );
    // 方法2:用nextTick
    // watch(
    //   list,
    //   async () => {
    //     await nextTick();
    //     const container = document.getElementById('list-container');
    //     if (container) {
    //       containerHeight.value = container.offsetHeight;
    //     }
    //   },
    //   { deep: true }
    // );
    return { list, containerHeight };
  }
}

两种方法都可以,看你习惯哪种。

优化5:合理使用onCleanup清理副作用

如果你的watch回调函数里有异步操作(比如发送请求、设置定时器),那一定要用onCleanup清理上一次的副作用,否则会导致上一次的请求结果覆盖这一次的,或者定时器一直运行。

举个发送请求的例子,比如根据输入的关键词搜索商品:

import { ref, watch } from 'vue';
import axios from 'axios';
export default {
  setup() {
    const keyword = ref('');
    const searchResults = ref([]);
    const isLoading = ref(false);
    watch(keyword, (newKeyword) => {
      // 如果关键词为空,清空结果,不发送请求
      if (!newKeyword.trim()) {
        searchResults.value = [];
        isLoading.value = false;
        return;
      }
      let isCancelled = false;
      const controller = new AbortController();
      // 设置加载状态
      isLoading.value = true;
      // 发送请求
      axios.get('https://api.example.com/search', {
        params: { q: newKeyword },
        signal: controller.signal
      }).then(res => {
        // 如果请求没有被取消,才更新结果
        if (!isCancelled) {
          searchResults.value = res.data;
          isLoading.value = false;
        }
      }).catch(err => {
        // 如果请求被取消,不处理错误;否则处理错误
        if (!axios.isCancel(err)) {
          console.error('搜索失败:', err);
          isLoading.value = false;
        }
      });
      // 清理函数:当上一次watch回调被触发时,会先执行这个清理函数
      watch.onCleanup(() => {
        isCancelled = true;
        controller.abort(); // 取消上一次的请求
      });
    }, { debounce: 500 }); // 哦对了,Vue3.4+还支持直接在options里加debounce和throttle选项!
    return { keyword, searchResults, isLoading };
  }
}

刚才的例子里用到了Vue3.4+新增的debounce选项,这个也是一个优化点!之前要加防抖节流,得自己用lodash的debounce/throttle包装回调函数,现在Vue3.4+直接支持在watch的第三个参数options里加debounce(毫秒数)或throttle(毫秒数)选项,非常方便。

咱们来做个小总结

今天咱们讲了Vue3 watch监听多个数据的所有核心内容:

  1. 三种基础写法:数组包ref/reactive顶层属性、数组包getter函数监听嵌套属性、watchEffect自动追踪(不算显式指定但很常用)。
  2. 三个进阶场景:多个需要深度对比的源(推荐用computed包装关键值)、特定条件触发、对应关系搞不清加注释。
  3. 五个常见坑点:直接监听reactive嵌套属性、直接监听整个reactive对象、deep模式下的新旧值引用问题、watchEffect的异步依赖、忘记停止watch。
  4. 五个优化点:能用computed就不用watch、用computed替代deep监听、合理使用immediate、合理使用flush、合理使用onCleanup和Vue3.4+的debounce/throttle。

其实不管是监听单个还是多个数据,核心都是“明确你要监听什么、什么时候触发、触发后要做什么”,只要抓住这三个点,再结合Vue3的官方文档和最佳实践,就能写出高效、清晰、不易出错的代码了。

如果还有什么疑问,或者有其他Vue3的问题想了解,欢迎在评论区留言哦!

版权声明

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

热门