Vue3的watch怎么同时监听多个数据?踩坑和优化点都给你理清楚了
很多刚上手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回调函数的执行时机,有三个可选值:
'pre'(默认值):在DOM更新之前执行回调函数。'post':在DOM更新之后执行回调函数,如果你的回调函数需要操作DOM(比如获取DOM元素的高度、宽度),那必须设置flush: 'post',或者用nextTick。'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监听多个数据的所有核心内容:
- 三种基础写法:数组包ref/reactive顶层属性、数组包getter函数监听嵌套属性、watchEffect自动追踪(不算显式指定但很常用)。
- 三个进阶场景:多个需要深度对比的源(推荐用computed包装关键值)、特定条件触发、对应关系搞不清加注释。
- 五个常见坑点:直接监听reactive嵌套属性、直接监听整个reactive对象、deep模式下的新旧值引用问题、watchEffect的异步依赖、忘记停止watch。
- 五个优化点:能用computed就不用watch、用computed替代deep监听、合理使用immediate、合理使用flush、合理使用onCleanup和Vue3.4+的debounce/throttle。
其实不管是监听单个还是多个数据,核心都是“明确你要监听什么、什么时候触发、触发后要做什么”,只要抓住这三个点,再结合Vue3的官方文档和最佳实践,就能写出高效、清晰、不易出错的代码了。
如果还有什么疑问,或者有其他Vue3的问题想了解,欢迎在评论区留言哦!
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网

