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

Vue3里watch到底能不能直接监听ref?有没有隐藏坑?最全指南来了

terry 2小时前 阅读数 44 #Vue

现在用Vue3开发项目的开发者越来越多,很多刚从Vue2转过来的朋友,或者刚开始学Vue3的新手,都会碰到一个绕不开的组合:watch和ref,有人说直接写就能用,有人说有时候数据变了watch没反应,还有人分不清什么时候用.value什么时候不用,今天就把这些问题掰扯得明明白白,连平时容易踩的小雷区都帮你挖出来踩平。

第一个核心问题:Vue3的watch真的可以直接监听ref吗?

答案是:大部分场景下可以直接传,完全没问题,但这里必须加个限定词“大部分场景”,后面会讲什么时候需要加.value或者换写法。

先回忆下Vue3里watch的基本用法,不管是组合式API里的import { watch, ref } from 'vue',还是<script setup>里的直接用,ref作为watch的第一个参数(监听源)有两种传法:第一种是直接传ref本身,第二种是传一个返回ref.value的函数。

举个最简单的例子,假设我们有个数字输入框,绑定了ref定义的count,要监听count的变化输出日志:

<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const inputChange = (e) => {
  count.value = Number(e.target.value)
}
// 写法1:直接传ref
watch(count, (newVal, oldVal) => {
  console.log('直接传ref的变化:新值', newVal, '旧值', oldVal)
})
// 写法2:传返回ref.value的函数
watch(() => count.value, (newVal, oldVal) => {
  console.log('传函数的变化:新值', newVal, '旧值', oldVal)
})
</script>
<template>
  <input type="number" :value="count" @input="inputChange" />
</template>

打开浏览器控制台,输入1、2、3,你会发现两种写法的日志都能正常触发,而且newVal和oldVal都是直接的数值,根本不用手动加.value——这就是Vue3组合式API的贴心之处,帮你做了一层ref的自动解包。

那这个解包是怎么实现的呢?其实可以简单理解为:watch在解析第一个监听源的时候,会先检查是不是ref类型的响应式数据,如果是,就自动把监听源改成了ref.value(而且不是你手动写的那种函数,Vue内部做了更高效的优化,不过对开发者来说效果差不多,但底层性能可能更好一点点)。

第二个关键问题:什么时候直接传ref会失效?必须加.value或者换写法?

刚才说了“大部分场景”,那剩下的少数场景是什么?这也是新手最容易踩坑的地方,我总结了三种最常见的失效情况:

场景1:监听的是ref.value本身的重新赋值以外的“深层变化”

这里先解释下什么是ref的“深层变化”,ref本质上是一个包装对象,它的.value才是真正的存储数据的地方——如果value是基本类型(数字、字符串、布尔值、null、undefined、Symbol),那只有重新给count.value赋值(比如count.value = 10,count.value = 'hello')才会触发响应式,这种时候不管用直接传ref还是传函数,watch都能正常工作;但如果value是引用类型(对象、数组、Map、Set),直接传ref的话,默认只能监听到ref.value被整个替换的情况(比如把user.value = { name: '李四' },但不能监听到user.value.name = '李四'或者user.value.push(1)这种内部属性/元素的变化)

举个对象的例子:

<script setup>
import { ref, watch } from 'vue'
const user = ref({
  name: '张三',
  age: 18
})
const changeName = () => {
  // 内部属性变化
  user.value.name = '李四'
}
const replaceUser = () => {
  // 整个替换ref.value
  user.value = { name: '王五', age: 20 }
}
// 直接传ref
watch(user, (newVal, oldVal) => {
  console.log('直接传ref监听user:新值', newVal, '旧值', oldVal)
})
</script>
<template>
  <div>姓名:{{ user.name }}</div>
  <div>年龄:{{ user.age }}</div>
  <button @click="changeName">修改姓名</button>
  <button @click="replaceUser">替换整个用户</button>
</template>

打开页面先点“修改姓名”,你会发现模板里的姓名变成了李四,但控制台没有任何日志输出;再点“替换整个用户”,日志才会出来——这就是失效的第一种情况。

那怎么解决呢?有两种方法:

  1. 保持直接传ref,但加上deep: true配置项;
  2. 传一个返回ref.value具体属性的函数(如果只需要监听某个属性),或者返回整个ref.value的函数再加deep: true(如果要监听所有深层变化)。

我们先试第一种解决方法:

// 修改直接传ref的watch
watch(user, (newVal, oldVal) => {
  console.log('直接传ref+deep=true监听user:新值', newVal, '旧值', oldVal)
}, {
  deep: true
})

现在再点“修改姓名”和“替换整个用户”,两次日志都会触发了——但这里要注意一个点:不管是直接传ref还是传函数,只要加了deep: true,oldVal都会变成和newVal一样的引用类型,因为引用类型在内存里是同一个地址,Vue没办法保存修改前的完整副本(除非你手动用JSON.parse(JSON.stringify())或者其他深拷贝方法存,但watch内部不会做这个,太耗性能了)。

再试第二种方法,假设我们只需要监听name属性的变化,不想因为age或者其他新增属性变化触发watch:

// 只监听user.value.name的函数写法
watch(() => user.value.name, (newVal, oldVal) => {
  console.log('只监听name属性的函数写法:新值', newVal, '旧值', oldVal)
})

现在点“修改姓名”,日志出来了,而且newVal和oldVal都是基本类型字符串,旧值是对的(张三),新值是李四;点“替换整个用户”的时候,如果新对象的name和旧的不一样(比如王五),这个watch也会触发——这就是只监听具体属性的好处,更精准,性能也更好(不用递归监听整个对象的所有属性)。

场景2:监听的是ref数组的索引或者长度以外的“部分元素变化”?不对,其实引用类型数组的所有内部变化(push、pop、splice、直接改索引)都属于深层变化,刚才的deep: true或者函数写法都能用,但这里要讲的是另一种容易混淆的情况:把ref拆成变量赋值之后再传watch

举个例子:

<script setup>
import { ref, watch } from 'vue'
const user = ref({ name: '张三' })
// 错误拆包!把ref.value直接赋值给了普通变量
const userName = user.value.name
const changeName = () => {
  user.value.name = '李四'
  // 此时userName还是'张三',因为普通变量不是响应式的
}
// 直接传普通变量userName,watch根本不知道要监听什么
watch(userName, (newVal, oldVal) => {
  console.log('传普通变量userName:', newVal, oldVal)
})
</script>

这个例子里的问题不是直接传ref的锅,是很多新手会犯的“错误拆包ref”——userName是普通的字符串变量,不是响应式的,不管你后面怎么改user.value.name,userName的值都不会变,watch当然也不会触发。

那正确的拆包或者传递变量的方式是什么?如果要在组合式API里传递ref相关的变量,要么传递整个ref本身(保持响应式包装),要么用toRef/toRefs把ref.value的属性变成响应式的ref,要么直接用计算属性computed

比如用toRefs把user的属性拆成响应式的ref:

<script setup>
import { ref, watch, toRefs } from 'vue'
const user = ref({ name: '张三', age: 18 })
// 正确拆包!用toRefs把每个属性变成响应式ref
const { name, age } = toRefs(user.value)
const changeName = () => {
  // 这里的name是ref,所以要加.value
  name.value = '李四'
}
// 直接传拆出来的响应式ref name
watch(name, (newVal, oldVal) => {
  console.log('传toRefs拆出来的name:', newVal, oldVal)
})
</script>
<template>
  <div>姓名:{{ name }}</div>
  <div>年龄:{{ age }}</div>
  <button @click="changeName">修改姓名</button>
</template>

现在点“修改姓名”,模板更新,控制台也有日志了——这里的name是toRefs生成的ref,所以可以直接传watch,而且在模板里不用加.value(因为模板里会自动解包顶层的ref和reactive的属性)。

场景3:监听的是Vue3内置的“非普通ref”?比如shallowRef或者customRef

刚才讲的都是普通ref,Vue3里还有两种和响应式深度相关的ref:shallowRef和customRef,这两种ref直接传watch的话,默认的监听逻辑会不一样,新手很容易忽略。

先讲shallowRef

shallowRef的意思是“浅层ref”——它只有.value本身的重新赋值会触发响应式,不管.value是基本类型还是引用类型,内部的任何变化都不会触发响应式,哪怕你加了deep: true也没用!

举个例子:

<script setup>
import { ref, watch, shallowRef } from 'vue'
const shallowUser = shallowRef({
  name: '张三',
  hobbies: ['读书']
})
const changeName = () => {
  // 内部属性变化,shallowRef不响应
  shallowUser.value.name = '李四'
}
const addHobby = () => {
  // 内部数组push,shallowRef不响应
  shallowUser.value.hobbies.push('打球')
}
const replaceShallowUser = () => {
  // 整个替换.value,shallowRef才响应
  shallowUser.value = { name: '王五', hobbies: ['画画'] }
}
// 直接传shallowRef,不管加不加deep: true,内部变化都不会触发
watch(shallowUser, (newVal, oldVal) => {
  console.log('直接传shallowUser:', newVal, oldVal)
}, {
  deep: true
})
</script>
<template>
  <div>姓名:{{ shallowUser.name }}</div>
  <div>爱好:{{ shallowUser.hobbies.join(', ') }}</div>
  <button @click="changeName">修改姓名</button>
  <button @click="addHobby">添加爱好</button>
  <button @click="replaceShallowUser">替换整个用户</button>
</template>

打开页面先点“修改姓名”和“添加爱好”,模板里的内容一点变化都没有(因为shallowRef内部不响应),控制台当然也没有日志;再点“替换整个用户”,模板更新,日志才出来——这就是shallowRef的特性,主要用来优化性能,比如存储一个不需要深层响应的大对象,避免递归监听所有属性带来的内存和CPU消耗。

那如果我想用shallowRef优化性能,但偶尔又需要监听它内部某个属性的变化怎么办?可以手动触发shallowRef的响应式更新,用Vue3提供的triggerRef函数:

<script setup>
import { ref, watch, shallowRef, triggerRef } from 'vue'
const shallowUser = shallowRef({
  name: '张三',
  hobbies: ['读书']
})
const changeName = () => {
  shallowUser.value.name = '李四'
  // 手动触发shallowUser的响应式更新
  triggerRef(shallowUser)
}
// 直接传shallowRef+triggerRef就能触发watch了
watch(shallowUser, (newVal, oldVal) => {
  console.log('直接传shallowUser+triggerRef:', newVal, oldVal)
})
</script>

现在点“修改姓名”,模板更新,控制台也有日志了——但triggerRef是手动触发的,一定要记得用,不然内部变化还是不会生效。

再讲customRef

customRef的意思是“自定义ref”——你可以完全控制ref的依赖收集和触发更新的逻辑,比如加防抖、节流,或者记录日志,这时候直接传customRef给watch能不能用,完全取决于你自己写的customRef的逻辑里有没有正确实现get和set函数里的track和trigger

举个带防抖的customRef例子:

<script setup>
import { ref, watch, customRef } from 'vue'
// 自定义带防抖的ref
function useDebouncedRef(value, delay = 500) {
  let timer
  return customRef((track, trigger) => {
    return {
      get() {
        // 依赖收集,告诉Vue这个ref被用到了
        track()
        return value
      },
      set(newVal) {
        // 清除之前的定时器
        clearTimeout(timer)
        // 延迟设置新值并触发更新
        timer = setTimeout(() => {
          value = newVal
          // 触发更新,告诉Vue这个ref的值变了
          trigger()
        }, delay)
      }
    }
  })
}
// 使用自定义防抖ref
const searchText = useDebouncedRef('')
// 模拟输入框输入后的搜索请求
watch(searchText, (newVal) => {
  if (newVal) {
    console.log('防抖500ms后搜索:', newVal)
  }
})
</script>
<template>
  <input type="text" v-model="searchText" placeholder="输入搜索内容" />
</template>

打开页面输入“1”、“2”、“3”,每次输入间隔如果小于500ms,控制台只会在最后一次输入后500ms输出一次“防抖500ms后搜索:123”——这说明直接传customRef给watch是可以正常工作的,因为我们在customRef的get函数里调用了track(收集依赖,让watch知道要监听这个ref),在set函数的定时器回调里调用了trigger(触发更新,让watch知道值变了)。

但如果我们不小心漏写了track或者trigger,那customRef直接传watch就会失效——比如漏写get函数里的track,watch根本不会监听这个ref;漏写set函数里的trigger,watch虽然监听了,但值变了不会触发回调。

第三个进阶问题:直接传ref和传返回ref.value的函数,到底有什么区别?

刚才很多例子里两种写法都能用,但它们之间还是有一些细微的区别,主要体现在以下三点:

区别1:底层的监听方式不同,直接传ref性能可能更好一点点

刚才说过,Vue3在解析直接传的ref监听源时,会做一层内部优化,直接把它绑定到ref的依赖更新上;而传函数的话,Vue会先执行一次这个函数,收集函数内部用到的所有响应式数据的依赖(不管是ref、reactive还是computed),然后当这些依赖中的任何一个变化时,都会重新执行函数,比较函数返回的新值和旧值,如果不一样就触发watch回调。

举个例子,假设我们有两个ref,count1和count2,然后写了一个传函数的watch,返回count1 + count2:

<script setup>
import { ref, watch } from 'vue'
const count1 = ref(0)
const count2 = ref(0)
// 传函数,收集count1和count2的依赖
watch(() => count1.value + count2.value, (newVal) => {
  console.log('count1+count2的和:', newVal)
})
</script>

这时候不管你改count1还是count2,函数都会重新执行,和也会变化,watch就会触发——但如果我们直接传count1,那就只会监听count1的变化,和count2没关系。

区别2:传函数可以监听多个响应式数据的组合变化,直接传ref不行(除非用数组传多个监听源)

刚才的例子其实已经体现了这一点,但这里可以再扩展一下:直接传ref可以传一个数组,数组里放多个ref或者reactive的属性(但reactive的属性不能直接传数组,要传函数),这样数组里的任何一个监听源变化,watch都会触发;但如果要监听这些监听源的组合结果(比如count1 > count2,count1 + count2 > 10),那就必须用传函数的写法。

举个传多个监听源的数组写法和传组合结果的函数写法的对比:

<script setup>
import { ref, watch } from 'vue'
const count1 = ref(0)
const count2 = ref(0)
const isShow = ref(false)
// 数组写法,监听count1、count2、isShow三个中的任何一个变化
watch([count1, count2, isShow], ([newCount1, newCount2, newIsShow], [oldCount1, oldCount2, oldIsShow]) => {
  console.log('数组写法监听三个源的变化:')
  console.log('count1:', oldCount1, '→', newCount1)
  console.log('count2:', oldCount2, '→', newCount2)
  console.log('isShow:', oldIsShow, '→', newIsShow)
})
// 组合结果的函数写法,只有当count1 > count2且isShow为true时才会触发(不管count1、count2、isShow单个怎么变)
watch(() => count1.value > count2.value && isShow.value, (newVal, oldVal) => {
  console.log('组合结果变化:', oldVal, '→', newVal)
})
</script>
<template>
  <div>count1:{{ count1 }} <button @click="count1++">+1</button></div>
  <div>count2:{{ count2 }} <button @click="count2++">+1</button></div>
  <div>isShow:{{ isShow }} <button @click="isShow = !isShow">切换</button></div>
</template>

打开页面可以自己试一下:比如先把count1加到3,count2加到2,isShow保持false,这时候数组写法会触发,但组合结果的函数写法不会(因为isShow是false);然后把isShow切换成true,组合结果的函数写法才会触发(从false变成true)——这就是传函数写组合结果的好处,可以实现更复杂的条件监听。

区别3:直接传ref时,watch内部拿到的是ref的包装对象,但自动解包了value;传函数时,watch内部拿到的是函数返回的原始值(不管是基本类型还是引用类型)

这个区别其实对开发者的日常使用影响不大,因为不管是哪种写法,watch回调里的newVal和oldVal都是我们想要的值——但如果你想在watch的immediate回调(立即执行一次)里访问ref本身的其他属性(比如customRef里自己加的属性),那直接传ref的写法里,watch的第一个参数可以拿到ref,但immediate回调里的newVal和oldVal还是value;而传函数的写法里,根本拿不到ref本身。

不过这种情况非常少见,一般只有在写复杂的自定义逻辑或者第三方库的时候才会用到,新手可以先不用管。

第四个实用问题:除了watch,Vue3里还有watchEffect和watchPostEffect、watchSyncEffect,它们监听ref的时候有什么不一样?

很多开发者只知道watch,不知道还有这三个effect函数,其实它们在监听ref(或者其他响应式数据)的时候,用法和逻辑都有很大的区别,这里简单对比一下,重点讲和ref相关的部分:

watch vs watchEffect

先讲最常用的两个:watch和watchEffect。

相同点:都能监听ref的变化,触发回调

不管是watch直接传ref、传函数,还是watchEffect,只要ref的值变了(普通ref是重新赋值或深层变化+deep,shallowRef是重新赋值或triggerRef,customRef是正确实现track和trigger),都会触发对应的回调。

不同点:

  1. 是否需要明确指定监听源:watch需要明确指定第一个参数作为监听源(直接传ref、传函数、传数组);watchEffect不需要,它会自动收集回调函数内部用到的所有响应式数据的依赖(不管是ref、reactive还是computed),只要依赖变化就触发回调。
  2. 是否能拿到旧值:watch能拿到newVal和oldVal(引用类型加deep: true除外);watchEffect拿不到旧值,只能拿到当前的新值。
  3. 是否有immediate配置项:watch默认不会立即执行,需要加immediate: true才会在组件初始化的时候执行一次;watchEffect默认就是立即执行一次的,不需要加任何配置项。
  4. 是否有flush配置项:虽然两者都有,但默认值不一样:watch的默认flush是'post'( DOM更新之后执行回调);watchEffect的默认flush也是'post',但它有一个专门的别名watchPostEffect,还有一个flush为'sync'的别名watchSyncEffect(同步执行回调,在DOM更新之前)。

举个watch和watchEffect监听searchText(刚才的customRef例子)的对比:

<script setup>
import { ref, watch, customRef, watchEffect } from 'vue'
function useDebouncedRef(value, delay = 500) {
  let timer
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newVal) {
        clearTimeout(timer)
        timer = setTimeout(() => {
          value = newVal
          trigger()
        }, delay)
      }
    }
  })
}
const searchText = useDebouncedRef('')
const searchCount = ref(0)
// watch:明确指定监听源searchText,有旧值,默认不立即执行,加immediate才执行
watch(searchText, (newVal, oldVal) => {
  if (newVal) {
    searchCount.value++
    console.log('watch搜索:', newVal, '(旧值:', oldVal, '),搜索次数:', searchCount.value)
  }
}, {
  immediate: true // 组件初始化时,如果searchText有值就执行
})
// watchEffect:自动收集searchText和searchCount的依赖?不,searchCount是在回调里赋值的,不是读取的,所以不会收集
// 只会收集searchText的依赖,因为回调里读取了searchText.value
watchEffect(() => {
  if (searchText.value) {
    // searchCount.value++是赋值,不是读取,不会触发watchEffect的依赖更新
    // 只有读取searchText.value才会触发
    console.log('watchEffect搜索:', searchText.value)
  }
})
</script>
<template>
  <input type="text" v-model="searchText" placeholder="输入搜索内容" />
  <div>搜索次数:{{ searchCount }}</div>
</template>

打开页面先看控制台,因为watch加了immediate,searchText初始是空的,所以不会输出;watchEffect默认立即执行,searchText也是空的,所以也不会输出,然后输入“1”,过500ms后,watch会输出“watch搜索:1 (旧值:''),搜索次数:1”,watchEffect会输出“watchEffect搜索:1”——这就是两者的区别。

watchEffect vs watchPostEffect vs watchSyncEffect

这三个其实是同一个东西的不同写法,区别只在于flush配置项:

  1. watchEffect:默认flush为'post',和watchPostEffect完全等价;
  2. watchPostEffect:flush为'post',在DOM更新之后执行回调——这是最常用的,比如你需要在DOM更新之后获取元素的尺寸或者位置;
  3. watchSyncEffect:flush为'sync',同步执行回调,在响应式数据变化之后、DOM更新之前——这个要慎用,因为会阻塞Vue的响应式更新流程,性能可能不好,一般只有在需要在DOM更新之前做一些操作(比如取消之前的动画、重置某些状态)的时候才会用到。

举个获取元素尺寸的例子,对比watchEffect(post)和watchSyncEffect:

<script setup>
import { ref, watchEffect, watchSyncEffect, onMounted } from 'vue'
const boxWidth = ref(0)
const boxRef = ref(null)
// watchEffect(post):在DOM更新之后执行,能拿到正确的尺寸
watchEffect(() => {
  if (boxRef.value) {
    boxWidth.value = boxRef.value.offsetWidth
    console.log('watchEffect(post)获取的boxWidth:', boxWidth.value)
  }
})
// watchSyncEffect:在DOM更新之前执行,组件初始化时boxRef.value可能还没挂载,offsetWidth是0
watchSyncEffect(() => {
  if (boxRef.value) {
    console.log('watchSyncEffect获取的boxWidth:', boxRef.value.offsetWidth)
  }
})
// 点击按钮修改box的宽度
const changeBoxWidth = () => {
  boxRef.value.style.width = `${Math.random() * 300 + 100}px`
}
</script>
<template>
  <div ref="boxRef" style="width: 200px; height: 100px; background-color: #f0f0f0; margin: 10px 0;"></div>
  <div>box的宽度:{{ boxWidth }}px</div>
  <button @click="changeBoxWidth">修改box宽度</button>
</template>

打开页面先看控制台,组件初始化时:

  • watchSyncEffect可能不会输出(因为boxRef.value还没挂载),或者输出0;
  • watchEffect(post)会在onMounted之后执行,输出200。

然后点击“修改box宽度”,先看watchSyncEffect的输出:它会在boxRef.value.style.width修改之后、DOM更新之前执行,所以拿到的还是旧的宽度(比如200);再看watchEffect(post)的输出:它会在DOM更新之后执行,拿到的是新的宽度(比如随机生成的250)——这就是flush配置项的区别。

第五个避坑指南:Vue3 watch refs的5个常见小错误,别再犯了

刚才讲了那么多理论和例子,现在总结一下新手最容易犯的5个小错误,帮你避坑:

错误1:监听引用类型的普通ref时,忘记加deep: true,导致内部变化不触发

这个是最常见的,刚才的场景1已经讲过了,解决方法就是要么加deep: true,要么传返回具体属性的函数。

错误2:把ref.value直接赋值给普通变量,然后传普通变量给watch

这个也是新手常犯的,场景2讲过,解决方法是要么传整个ref,要么用toRef/toRefs拆成响应式ref,要么用computed。

错误3:监听shallowRef的内部变化时,加了deep: true但忘记triggerRef

场景3讲过shallowRef的特性,deep: true对它没用,必须手动用triggerRef触发更新。

错误4:写customRef时,漏写get函数里的track或者set函数里的trigger

场景3讲过customRef的核心是track和trigger,漏写任何一个都会导致响应式失效或者watch不触发。

错误5:在watch的回调里修改正在监听的ref,导致无限循环

<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// 错误!在回调里修改count,会导致无限循环
watch(count, (newVal) => {
  count.value++
  console.log('无限循环的count:', newVal)
})
</script>

打开页面你会发现控制台会一直输出,直到浏览器崩溃——解决方法是要么不要在回调里修改正在监听的ref,要么加个判断条件,比如只有当newVal满足某个条件时才修改,要么用watchEffect但小心依赖收集(比如不要在回调里同时读取和修改同一个ref)。

Vue3 watch refs的正确打开方式

我们来总结一下Vue3 watch refs的正确打开方式,方便大家记忆:

  1. 基本类型的普通ref:直接传ref,或者传返回ref.value的函数,两种写法都可以,不用加deep: true;
  2. 引用类型的普通ref
    • 如果要监听整个ref.value的替换:直接传ref,或者传返回ref.value的函数;
    • 如果要监听所有深层变化:直接传ref加deep: true,或者传返回ref.value的函数加deep: true;
    • 如果要监听某个具体属性的变化:传返回该具体属性的函数(推荐,更精准,性能更好);
  3. shallowRef
    • 只能监听整个ref.value的替换:直接传shallowRef;
    • 如果要偶尔监听内部变化:直接传shallowRef,修改内部属性后手动调用triggerRef;
  4. customRef:直接传customRef,但要确保自己写的get函数里有track,set函数里有trigger;
  5. 监听多个ref的组合变化:传返回组合结果的函数;
  6. 监听多个ref的单个变化:传包含这些ref的数组;
  7. 不需要旧值,需要自动收集依赖,需要立即执行:用watchEffect(或者watchPostEffect);
  8. 需要在DOM更新之前执行:用watchSyncEffect。

好了,今天关于Vue3 watch refs的内容就讲这么多,应该把所有常见的问题和坑都覆盖到了,如果你还有其他疑问,可以在评论区留言,我们一起讨论。

版权声明

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

热门