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

Vue3里怎么同时监听多个数据源?不同监听方式有啥区别?踩过哪些坑?

terry 1小时前 阅读数 12 #Vue

平时做Vue3项目开发,经常会遇到这种情况:表单提交前要同时检查用户名、密码、邮箱是否都不为空,或者筛选列表页要监听关键词、分类、排序方式三个变量同时触发数据请求,再或者状态管理里要拿pinia/vuex的两个state来计算某个临时值的变化,这些都得用到多源监听,很多刚从Vue2转过来或者刚学Vue3的朋友,可能只会一股脑把多个变量塞数组里,但其实Vue3里多源监听有好几种写法,每种都有适用场景,稍不注意还会踩坑,今天就把这些事儿说透。

先搞懂:Vue3 watch支持的“源”到底有哪些?

在讲多源监听之前,得先单拎出来watch的“源”类型说清楚——因为不是所有东西都能直接丢进去给watch当监听对象的,不同类型的源在多源场景下的表现也不一样,这是基础中的基础。 ref和reactive的属性肯定是最常用的,比如const username = ref('')或者const form = reactive({ password: '' }),这时候直接写username或者() => form.password就能当源;然后是getter函数,也就是有返回值的箭头函数,比如() => [username.value, form.password]这种;还有computed属性,不管是带get的还是有get有set的,都可以;最后是一个数组,里面可以混合放前面说的所有合法源,这就是我们常说的“数组写法”多源监听。

第一种多源监听方式:数组写法(最基础、最常用)

很多朋友第一次接触Vue3多源监听,用的都是这个方法:把要监听的所有ref、computed或者getter函数丢进watch的第一个参数数组里,第二个参数是回调函数,回调函数的第一个参数是新值组成的数组,顺序和第一个参数数组的源顺序一致,第二个参数是旧值组成的数组,顺序也一样。 举个最常见的筛选列表的例子吧: 假设我们有三个筛选条件的ref,分别是keyword(关键词)、categoryId(分类ID)、sortType(排序方式,price_asc'、'time_desc'),然后想只要这三个里有一个变,就重新请求商品列表,这时候就可以这么写:

import { ref, watch } from 'vue'
const keyword = ref('')
const categoryId = ref(0)
const sortType = ref('time_desc')
const getProductList = async () => {
  // 这里是请求接口的逻辑,比如用axios
  // const res = await axios.get('/api/products', { 
  //   params: { keyword, categoryId, sortType } 
  // })
  console.log('触发了商品列表请求,当前条件:', [keyword.value, categoryId.value, sortType.value])
}
// 数组写法多源监听
watch([keyword, categoryId, sortType], ([newKey, newCate, newSort], [oldKey, oldCate, oldSort]) => {
  // 这里可以处理新旧值对比,比如分类变了但关键词没变,要不要清空页码?
  if (newCate !== oldCate) {
    console.log('分类变了,清空页码')
    // page.value = 1
  }
  getProductList()
})

这种写法的优点很明显:直观,一眼就能看出来监听了哪些源;新旧值按顺序返回,对比的时候很方便;如果需要单独给某个源加deep或者immediate这些配置?不行哦——数组写法里配置是全局的,整个数组的源要么都deep要么都不deep,要么都immediate要么都不immediate,这是它的第一个小局限。 还有第二个小细节:如果数组里的某个源是reactive对象本身,不是它的属性或者getter函数,那默认就是deep监听的,不管有没有加deep: true配置,而且旧值会和新值一样——因为reactive是响应式对象的引用,Vue3不会保留它的历史快照,除非你手动深拷贝一下旧值,但这样性能可能会有问题,所以尽量不要直接监听reactive对象本身,不管是单源还是多源。 比如你直接写watch([form], ([newForm], [oldForm]) => {}),这里的newForm和oldForm永远是同一个对象引用,对比不了;但如果写成watch([() => form], ([newForm], [oldForm]) => {}),旧值就可以保留,但如果form内部嵌套很深,这时候又要记得加deep: true,不然只有form的顶层属性(比如直接给form赋值一个新对象{})才会触发监听,form内部的password变了不会触发。

第二种多源监听方式:用getter函数返回一个对象/数组(自定义新旧值触发规则的利器)

刚才说数组写法的配置是全局的,那如果我想监听三个源,但只有其中的keyword和categoryId变了才触发请求,sortType变了不触发?或者只有三个源都不为空才触发?或者想把新旧值组织成一个对象,不用按顺序记?这时候就可以用getter函数当watch的第一个源参数,返回一个包含多个源的数组或者对象,这其实也算是“多源监听”,因为getter函数的返回值依赖了多个响应式数据嘛。

场景1:自定义触发条件(比如只监听部分源,或者只在特定值变化时触发)

还是刚才的筛选列表例子,如果现在产品经理说:“sortType变了的话,我们前端直接用现有数据排就行,不用重新请求接口,节省服务器资源”,那用数组写法就没办法了,因为一变化就会触发,但用getter函数就可以:

import { ref, watch, computed } from 'vue'
// 前面的keyword、categoryId、sortType定义不变
// 方法一:在getter里只返回需要触发请求的源,这样sortType变了getter的返回值不变,watch就不会触发
watch(() => [keyword.value, categoryId.value], ([newKey, newCate]) => {
  // 对比逻辑不变
  if (newCate !== oldKey?不对,是和旧Cate比,第二个参数的旧值数组顺序和返回的一致
  // 哦对,重新写回调参数
  ([newKey, newCate], [oldKey, oldCate]) => {
    if (newCate !== oldCate) {
      console.log('分类变了,清空页码')
    }
    getProductList()
  }
)
// 那如果还有更复杂的触发条件呢?比如只有keyword长度大于等于2,或者categoryId大于0,这两个条件同时满足至少一个变化才触发?
// 可以用computed先包装一个“是否需要请求”的条件,不过也可以直接在getter的返回值里做判断,或者用watch的第三个参数里的flush和immediate之外的配置?不对,Vue3 watch有一个配置项叫`watchOptions`里的`...`哦不对,Vue3原生watch没有直接的`filter`配置,不过可以在getter里返回一个“触发标识”,或者在回调里加判断,但更优雅的是用computed先处理成需要监听的依赖:
const shouldRequestTrigger = computed(() => {
  // 这里返回一个依赖了keyword和categoryId的值,sortType完全不依赖,所以sortType变了不会更新这个computed
  return {
    key: keyword.value.length >= 2 ? keyword.value : '', // 长度不够的话key不变,相当于不触发关键词的监听
    cate: categoryId.value
  }
})
watch(shouldRequestTrigger, (newVal, oldVal) => {
  // 现在只有shouldRequestTrigger的返回值真的变了才会触发,比如keyword从1变成2(触发key变化),或者categoryId从0变成1(触发cate变化),或者两个同时变
  console.log('触发了有效条件的商品列表请求,当前有效条件:', newVal)
  // 对比逻辑
  if (newVal.cate !== oldVal.cate) {
    console.log('分类变了,清空页码')
  }
  getProductList()
}, {
  // 这里要加deep吗?如果shouldRequestTrigger返回的是对象,默认是浅监听,也就是只有newVal和oldVal的引用变了才触发,但shouldRequestTrigger每次依赖更新都会返回一个新对象(因为{}是字面量),所以默认不加deep也会触发
  // 但如果是返回数组也是一样的,[]每次都是新的
  // 不过如果返回的是一个原始值组成的数组或者单个原始值,当然不用deep
})

场景2:把新旧值组织成对象,不用按顺序记

刚才数组写法的回调参数是按源的顺序排列的,有时候源多了(比如5、6个),很容易搞混哪个新值对应哪个旧值,用getter返回对象的话,回调参数的newVal和oldVal就是有键名的对象,用起来就舒服多了:

import { ref, watch } from 'vue'
const username = ref('')
const password = ref('')
const email = ref('')
const phone = ref('')
watch(() => ({
  username: username.value,
  password: password.value,
  email: email.value,
  phone: phone.value
}), (newForm, oldForm) => {
  console.log('新的表单值:', newForm)
  console.log('旧的表单值:', oldForm)
  // 直接用键名,不用记顺序,比如只检查email的变化:
  if (newForm.email !== oldForm.email) {
    console.log('邮箱变了,重置验证码')
    // code.value = ''
  }
}, {
  deep: true, // 这里的deep其实加不加都行?因为返回的是字面量对象,每次依赖更新都是新引用,浅监听也能触发;但如果返回的是reactive的某个嵌套属性组成的子对象?不对,getter里是.value或者() => form.userInfo这种嵌套属性的话,如果返回的是form.userInfo这个reactive子对象本身,那又要加deep了,而且旧值和新值引用一样,所以最好还是把嵌套属性拆解成原始值或者getter里的属性值返回
})

这里再提醒一下刚才说的reactive源的问题:如果getter里返回的是() => form(整个reactive对象),那必须加deep才能监听内部属性变化,但旧值和新值还是同一个引用;如果返回的是() => ({ ...form })(浅拷贝整个reactive对象),那不加deep也能监听顶层属性变化,内部嵌套属性变化还是要加deep,而且旧值的顶层属性是可以对比的,嵌套属性还是引用;如果想要完全对比新旧嵌套对象的属性,那只能在回调里手动深拷贝旧值,或者在getter里返回深拷贝后的对象,但深拷贝如果是大对象的话,性能会很差,所以尽量不要监听整个大的嵌套reactive对象,而是拆分成小的ref或者getter监听具体的属性。

第三种多源监听方式:watchEffect(“自动收集依赖”的懒人写法)

刚才的数组写法和getter写法都是“显式指定依赖”,也就是你要手动把所有要监听的源丢进去,或者写在getter函数的返回值里;而watchEffect是“自动收集依赖”,你不用指定要监听谁,只要在watchEffect的回调函数里用到了哪些响应式数据,它就会自动监听这些数据,只要其中一个变了,就会重新执行回调函数——这其实也是多源监听的一种,而且代码更短,适合那种不需要对比新旧值,只要依赖变了就执行的场景。 还是拿筛选列表请求的例子,不过这次我们不需要对比新旧分类清空页码(虽然也可以在watchEffect里自己存旧值对比,但稍微麻烦一点),只要三个筛选条件变了就请求:

import { ref, watchEffect } from 'vue'
// 前面的keyword、categoryId、sortType定义不变
watchEffect(() => {
  // 只要在回调里用到了这三个ref的.value,watchEffect就会自动监听它们
  console.log('watchEffect触发了商品列表请求,当前条件:', [keyword.value, categoryId.value, sortType.value])
  getProductList()
})

你看,代码是不是比数组写法短多了?而且不用担心漏加源,只要用到了就会监听,那它有什么缺点呢? 刚才说的,默认没有新旧值对比,如果需要对比的话,得自己手动存旧值;默认是立即执行的(也就是immediate: true),如果你不想页面刚加载就执行,得用watchEffect的另一个兄弟API——watchPostEffect或者watchSyncEffect?不对,这三个的区别是执行时机,watchEffect默认是flush: 'pre',在组件更新前执行;watchPostEffect是flush: 'post',在组件更新后执行;watchSyncEffect是flush: 'sync',同步执行(性能最差,尽量不用);但这三个默认都是立即执行的,没有immediate: false的配置,如果想要延迟执行,得自己加个flag,

import { ref, watchEffect, onMounted } from 'vue'
const isMounted = ref(false)
onMounted(() => {
  isMounted.value = true
})
watchEffect(() => {
  if (!isMounted.value) return // 页面没挂载完不执行
  console.log('延迟到挂载后才执行的watchEffect')
  getProductList()
})

第三个缺点,没有显式的依赖列表,代码可读性稍微差一点——尤其是当watchEffect的回调函数很长的时候,你得扫一遍整个回调才知道它监听了哪些源,而数组写法和getter写法一眼就能看出来;第四个缺点,如果不小心在回调里用到了不该监听的响应式数据,就会导致不必要的执行——比如你在回调里不小心写了个console.log(loading.value),而loading是另一个控制加载动画的ref,那loading变了也会触发watchEffect重新请求接口,这肯定不是我们想要的,所以用watchEffect的时候要特别注意,回调里只放和监听逻辑相关的响应式数据。 那什么时候用watchEffect呢?

  1. 页面刚加载就要执行,而且不需要对比新旧值的场景,比如请求初始化数据(当然也可以用数组写法加immediate: true,不过watchEffect更短);
  2. 依赖的源很多,而且容易漏加的场景,比如表单的自动保存,只要表单里的任何一个输入框变了,就自动保存到本地存储localStorage里,这时候用watchEffect自动收集所有表单输入框的依赖,比手动把所有输入框的ref丢数组里方便多了;
  3. 需要根据多个响应式数据动态生成DOM或者其他副作用,但不需要对比新旧值的场景。

补充:多个watch vs 单个多源watch,该选哪个?

刚才讲的都是单个watch监听多个源,那有没有必要拆成多个单个源的watch呢?比如刚才的表单例子,监听username变了检查是否重复,监听password变了检查强度,监听email变了检查格式,这时候肯定是拆成多个单个源的watch更好,因为每个watch的逻辑是独立的,互不干扰,代码可读性和可维护性都更高;但如果是刚才的筛选列表例子,多个源变了要执行同一个逻辑(请求接口),那肯定是用单个多源watch更好,代码更简洁,而且不会因为多个源同时变化而触发多次执行(比如用户同时快速修改了keyword和categoryId,多个单个源的watch会触发两次请求接口,而单个多源watch只会触发一次——这里要注意,Vue3的响应式系统有批量更新机制,多个源在同一个事件循环里变化的话,不管是单个多源watch还是多个单个源的watch,都只会批量触发一次,但如果是不同的事件循环里的变化,比如先改keyword,过100ms再改categoryId,那多个单个源的watch就会触发两次,而单个多源watch也会触发两次,但至少逻辑是统一在一个回调里的,不用写重复的请求接口代码)。 总结一下选择原则:

  • 如果多个源变了要执行同一个、统一的逻辑,而且不需要单独控制每个源的配置(比如有的要deep有的不要),就用单个多源watch
  • 如果多个源变了要执行不同的、独立的逻辑,或者需要单独控制每个源的配置,就用多个单个源的watch
  • 如果多个源变了要执行同一个逻辑,但需要延迟执行或者对比部分新旧值,可以用单个多源watch的getter写法
  • 如果多个源变了要执行同一个逻辑,不需要对比新旧值,而且页面刚加载就要执行,可以用watchEffect

常见踩坑指南,别再犯了!

刚才在讲每种写法的时候都提到了一些小细节,现在把它们整理成常见的踩坑指南,都是我自己和身边同事在开发中遇到过的:

坑1:直接监听reactive对象本身,导致旧值和新值一样,而且默认deep

这个刚才说了好几次了,再强调一遍:尽量不要直接监听reactive对象本身,不管是单源还是多源!如果要监听整个reactive对象的所有属性变化,就在getter里返回它的浅拷贝或者深拷贝(看需求),或者拆分成小的ref监听具体的属性。

坑2:数组写法里混合了reactive对象本身和其他源,导致全局deep生效

比如你写watch([username, form], ...),这里form是reactive对象本身,那默认整个watch的配置就是deep: true,哪怕你不加,而且username的旧值是正常的(因为是ref),但form的旧值和新值引用一样,如果你不需要监听form的内部属性,只需要监听form的顶层引用变化(比如直接给form赋值{}),那应该把form改成() => form的getter写法,这样全局deep就不会默认生效了。

坑3:watchEffect里用到了不该监听的响应式数据,导致不必要的执行

比如刚才说的在watchEffect里不小心写了console.log(loading.value),那loading变了也会触发执行,解决办法是:要么把loading的.value改成loading(但这样就不是响应式的了),要么用watchEffect的onCleanup函数?不对,onCleanup是用来清理副作用的,不是用来取消依赖的;要么就把不需要监听的响应式数据放在watchEffect外面,或者拆分成显式指定依赖的watch。

坑4:getter写法返回的是reactive的嵌套属性组成的子对象,没有加deep,导致内部属性变化不触发

比如你写const form = reactive({ userInfo: { name: '', age: 0 } }),然后watch(() => form.userInfo, ...),这里form.userInfo是reactive的子对象,默认是浅监听,所以只有直接给form.userInfo赋值{}才会触发,form.userInfo.name变了不会触发,解决办法是要么加deep: true(但旧值和新值引用一样),要么把getter改成() => ({ name: form.userInfo.name, age: form.userInfo.age })(这样返回的是字面量对象,每次属性变化都是新引用,不加deep也能触发,而且旧值的属性可以对比)。

坑5:数组写法里的回调参数顺序搞混

这个虽然不是技术问题,但很容易犯,尤其是源多了的时候,解决办法是要么用getter返回对象的写法,要么在回调参数里加注释,

watch([keyword, categoryId, sortType], (
  [newKeyword, newCategoryId, newSortType], // 新值顺序:关键词、分类ID、排序方式
  [oldKeyword, oldCategoryId, oldSortType]  // 旧值顺序同上
) => {
  // 逻辑
})

坑6:watchEffect的执行时机不对,导致获取不到DOM

比如你在watchEffect里想要获取某个DOM元素的高度,但watchEffect默认是flush: 'pre',在组件更新前执行,这时候DOM可能还没渲染完或者更新完,获取到的高度是旧的或者undefined,解决办法是用watchPostEffect(flush: 'post'),它会在组件更新后执行,这时候DOM已经渲染完或者更新完了。

实战案例:用Vue3多源监听做一个带实时验证的注册表单

现在把刚才讲的所有知识点整合起来,做一个带实时验证的注册表单,包含以下功能:

  1. 监听username(ref):长度在3-20之间,且不能包含特殊字符;
  2. 监听password(ref):长度在6-20之间,且必须包含字母和数字;
  3. 监听confirmPassword(ref):必须和password一致;
  4. 监听email(ref):必须符合邮箱格式;
  5. 监听以上四个源的所有有效变化(比如username长度从2变成3才算有效变化):只有四个都通过验证,才显示“注册”按钮,否则显示“请完善信息”提示。 这个案例里,既有多个单个源的watch(分别做每个输入框的验证),又有单个多源watch(监听所有验证结果,控制注册按钮的显示),非常适合练手。 准备HTML模板:
    <template>
    <div class="register-form">
     <h2>用户注册</h2>
     <div class="form-item">
       <label>用户名:</label>
       <input v-model="username" type="text" placeholder="请输入3-20位字母、数字或下划线" />
       <span v-if="usernameError" class="error">{{ usernameError }}</span>
     </div>
     <div class="form-item">
       <label>密码:</label>
       <input v-model="password" type="password" placeholder="请输入6-20位包含字母和数字的密码" />
       <span v-if="passwordError" class="error">{{ passwordError }}</span>
     </div>
     <div class="form-item">
       <label>确认密码:</label>
       <input v-model="confirmPassword" type="password" placeholder="请再次输入密码" />
       <span v-if="confirmPasswordError" class="error">{{ confirmPasswordError }}</span>
     </div>
     <div class="form-item">
       <label>邮箱:</label>
       <input v-model="email" type="email" placeholder="请输入有效邮箱" />
       <span v-if="emailError" class="error">{{ emailError }}</span>
     </div>
     <div class="form-tip">
       <span v-if="!canRegister" class="tip-error">请完善信息</span>
       <span v-else class="tip-success">信息填写完整,可以注册</span>
     </div>
     <button :disabled="!canRegister" class="register-btn">注册</button>
    </div>
    </template>

    准备JavaScript逻辑:

    import { ref, watch, computed } from 'vue'

export default { name: 'RegisterForm', setup() { // 1. 定义响应式数据源 const username = ref('') const password = ref('') const confirmPassword = ref('') const email = ref('')

// 2. 定义验证错误的ref
const usernameError = ref('')
const passwordError = ref('')
const confirmPasswordError = ref('')
const emailError = ref('')
// 3. 定义多个单个源的watch,分别做验证
// 验证用户名
const validateUsername = (val) => {
  if (!val) {
    usernameError.value = ''
    return false
  }
  if (val.length < 3 || val.length > 20) {
    usernameError.value = '用户名长度必须在3-20之间'
    return false
  }
  const reg = /^[a-zA-Z0-9_]+$/
  if (!reg.test(val)) {
    usernameError.value = '用户名只能包含字母、数字或下划线'
    return false
  }
  usernameError.value = ''
  return true
}
watch(username, validateUsername, { immediate: true }) // immediate: true,页面刚加载就验证
// 验证密码
const validatePassword = (val) => {
  if (!val) {
    passwordError.value = ''
    return false
  }
  if (val.length < 6 || val.length > 20) {
    passwordError.value = '密码长度必须在6-20之间'
    return false
  }
  const reg = /^(?=.*[a-zA-Z])(?=.*\d).+$/
  if (!reg.test(val)) {
    passwordError.value = '密码必须包含字母和数字'
    return false
  }
  passwordError.value = ''
  return true
}
watch(password, validatePassword, { immediate: true })
// 验证确认密码(这里要同时监听password和confirmPassword,所以用多源数组写法)
const validateConfirmPassword = (newVal) => {
  // newVal是[newPassword, newConfirmPassword],但这里我们不需要旧值,也不需要新密码,只要确认密码和当前密码对比就行
  const currentPwd = password.value
  const currentConfirmPwd = confirmPassword.value
  if (!currentConfirmPwd) {
    confirmPasswordError.value = ''
    return false
  }
  if (currentConfirmPwd !== currentPwd) {
    confirmPasswordError.value = '两次输入的密码不一致'
    return false
  }
  confirmPasswordError.value = ''
  return true
}
watch([password, confirmPassword], validateConfirmPassword, { immediate: true })
// 验证邮箱
const validateEmail = (val) => {
  if (!val) {
    emailError.value = ''
    return false
  }
  const reg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  if (!reg.test(val)) {
    emailError.value = '请输入有效邮箱'
    return false
  }
  emailError.value = ''
  return true
}
watch(email, validateEmail, { immediate: true })
// 4. 定义单个多源watch或者computed,监听所有验证结果,控制注册按钮的显示
// 这里用computed更合适,因为canRegister只是一个计算属性,没有副作用,不需要watch
const canRegister = computed(() => {
  // 直接调用刚才的验证函数,确保每次计算都是最新的验证结果
  const isUsernameValid = validateUsername(username.value)
  const isPasswordValid = validatePassword(password.value)
  const isConfirmPasswordValid = validateConfirmPassword()
  const isEmailValid = validateEmail(email.value)
  return isUsernameValid && isPasswordValid && isConfirmPasswordValid && isEmailValid
})
// 5. 可以在这里加注册按钮的点击事件,不过不是今天的重点,就省略了
return {
  username,
  password,
  confirmPassword,
  email,
  usernameError,
  passwordError,
  confirmPasswordError,
  emailError,
  canRegister
}
准备一点简单的CSS样式,让页面好看一点:
```css
.register-form {
  width: 400px;
  margin: 50px auto;
  padding: 30px;
  border: 1px solid #eee;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.register-form h2 {
  text-align: center;
  margin-bottom: 30px;
  color: #333;
}
.form-item {
  margin-bottom: 20px;
}
.form-item label {
  display: inline-block;
  width: 80px;
  text-align: right;
  margin-right: 10px;
  color: #666;
}
.form-item input {
  width: 280px;
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}
.form-item input:focus {
  outline: none;
  border-color: #409eff;
}
.error {
  display: block;
  margin-top: 5px;
  margin-left: 90px;
  font-size: 12px;
  color: #f56c6c;
}
.form-tip {
  margin-bottom: 20px;
  text-align: center;
  font-size: 14px;
}
.tip-error {
  color: #f56c6c;
}
.tip-success {
  color: #67c23a;
}
.register-btn {
  display: block;
  width: 100%;
  padding: 10px 0;
  background-color: #409eff;
  color: #fff;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
}
.register-btn:disabled {
  background-color: #a0cfff;
  cursor: not-allowed;
}

这个案例里,我们用到了单个源的watch、多源数组写法的watch、computed(其实computed也是自动收集依赖的,和watchEffect类似,不过它是有返回值的,用来做计算属性,没有副作用),还避开了刚才说的所有坑,大家可以自己复制到Vue3项目里试试,效果应该还不错。

最后总结一下

Vue3里同时监听多个数据源的方式主要有三种:数组写法(显式指定依赖,全局配置,新旧值按顺序返回)、getter返回对象/数组写法(显式指定依赖,自定义触发规则和新旧值组织方式)、watchEffect(自动收集依赖,立即执行,代码简洁),还要根据需求选择多个单个源的watch还是单个多源watch,以及避开常见的坑。 希望这篇文章能帮到大家,要是还有什么不懂的地方,或者遇到了其他Vue3多源监听的问题,欢迎在评论区留言讨论!

版权声明

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

热门