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

Vue3中onMounted获取数据后watch怎么没触发?踩过这些坑才算入门Vue3响应式

terry 2小时前 阅读数 26 #Vue
文章标签 Vue3 watch触发

作为前端入门Vue3的人,是不是经常遇到这种抓狂的场景:页面刚用onMounted调了接口拿到最新数据,明明变量是ref或者reactive包的,给它赋值后写好的watch就是纹丝不动?或者有时候动了有时候不动,根本摸不着规律?别慌,这其实是对Vue3响应式系统和watch的使用细节没吃透,今天咱们就把这些问题掰开揉碎说清楚。

先搞懂Vue3的响应式是什么,是watch能工作的基础

很多人学Vue3的时候直接跳过响应式原理的“大概了解”部分,上来就写代码,其实这才是踩坑的根源,Vue2用的是Object.defineProperty劫持对象属性,数组还得靠重写7个方法实现响应式;但Vue3不一样,它完全换了个思路——用Proxy代理整个对象/数组,配合Reflect反射处理数据操作,监听的维度直接从“单个属性/重写数组方法”升到了“整个对象/数组的所有访问、赋值、删除等操作”,不过Proxy虽然强大,但也有它的“小脾气”,这些小脾气直接影响watch的触发,接下来咱们先举两个最简单的例子,对比看一下响应式的区别:

// 先看ref的例子(不管ref包的是基本类型还是引用类型,Vue3都会把.value处理成响应式)
const count = ref(0)
const updateCount = () => count.value++
const watchRef = () => watch(count, (newVal) => console.log('ref count变了:', newVal))
updateCount() // 会触发watchRef
watchRef() // 注意哦,如果这里先赋值再watch,watch第一次也不会打印——这后面会讲
// 再看reactive的“坑”
const data = reactive({ name: '小明', friends: ['小红', '小刚'] })
const updateName = () => data.name = '小华' // 这个没问题,Proxy能监听到属性赋值
const resetData = () => data = { name: '小李', friends: [] } // 哦豁,这里resetData执行后,updateName再改data.name,watch根本不会触发
const addFriend = () => data.friends.push('小王') // 数组push没问题,Proxy监听到了
const replaceFriends = () => data.friends = ['小赵'] // 替换整个数组没问题,因为是data的属性赋值,不是直接替换reactive包裹的变量
const watchReactive1 = () => watch(data, (newVal) => console.log('reactive整体变了:', newVal))
const watchReactive2 = () => watch(() => data.friends, (newVal) => console.log('friends变了:', newVal))
updateName() // 触发watchReactive1,不触发watchReactive2
addFriend() // 两个都触发
replaceFriends() // 两个都触发
resetData() // watchReactive1第一次可能会(如果data之前有变化的话),之后updateName/watchReactive1都不会,data已经变成普通对象了,Proxy丢了

从上面的例子就能看出几点关于watch的前提:第一,被监听的必须是Vue3官方提供的响应式引用类型,或者返回响应式引用类型的getter函数;第二,不能直接替换reactive包裹的顶层变量,只能替换它的属性或者修改属性内部的引用类型数据;第三,还有个“监听时机”的小细节——先赋值再开启watch,第一次赋值不会触发(除非设置了immediate),这些前提,结合onMounted获取数据的场景,就会引出咱们接下来要讲的核心踩坑点。

核心踩坑1:onMounted里赋值的“方式不对”,导致响应式丢了

这是新手最常犯的错误,尤其是从Vue2转过来的人,习惯了“先定义个空对象,接口回来直接把整个接口返回的data赋值过去”,但在Vue3里用reactive这么写,大概率就会出问题——咱们先看一段错误代码:

// Vue3组件setup部分(假设是script setup语法,因为这是现在主流写法)
<script setup>
import { reactive, watch, onMounted } from 'vue'
// 错误写法1:直接定义reactive空对象,接口回来直接替换
const userInfo = reactive({})
onMounted(async () => {
  // 模拟接口请求
  const res = await new Promise(resolve => setTimeout(() => resolve({ id: 1, name: '接口返回的小明', age: 25 }), 1000))
  // 重点错误!直接替换reactive顶层变量userInfo,Proxy代理就丢了,userInfo变成了普通对象
  userInfo = res.data // 哦不对,res直接返回data吧?不管,哪怕res.data是普通对象,替换后就完了
})
// 定义watch监听userInfo
watch(userInfo, (newVal) => console.log('userInfo变了:', newVal), { deep: true }) // 哪怕加了deep也没用,因为根本不是响应式了
</script>

那这段代码如果用Vue2的Object.defineProperty会不会有问题?其实也会有隐患,但Vue2有时候可能会“运气好”触发一次更新(因为如果组件里直接用了userInfo,可能Vue2的render函数里的依赖收集还在,但接口回来替换后新的属性没有被defineProperty劫持,后面再改就不行了),但Vue3的Proxy是“一次性绑定顶层变量的引用地址”,一旦你把userInfo的引用地址换成了普通对象的地址,Proxy就彻底失去作用了。

那正确的写法有几种呢?咱们分ref和reactive两种情况说:

用reactive的正确写法

既然不能替换reactive的顶层变量,那咱们可以用两种方法:一种是“把接口返回的数据拆开来赋值给reactive的属性”,另一种是“用reactive包裹一个带目标属性的容器,然后只替换容器里的目标属性”——两种方法都可以,看你的接口返回数据结构是怎样的:

<script setup>
import { reactive, watch, onMounted } from 'vue'
// 正确写法1.1:reactive容器里留好所有接口可能返回的属性,或者不管有没有,直接赋值(Proxy会处理新增属性!这点比Vue2强太多)
const userInfo = reactive({}) // 哪怕是空的也没关系,后面可以直接加id、name这些属性
onMounted(async () => {
  const res = await new Promise(resolve => setTimeout(() => resolve({ id: 1, name: '接口返回的小明', age: 25, address: { city: '北京' } }), 1000))
  // 方法1:逐个赋值(适合数据结构简单的)
  // userInfo.id = res.id
  // userInfo.name = res.name
  // userInfo.age = res.age
  // userInfo.address = res.address
  // 方法2:用Object.assign(适合数据结构复杂,不想逐个写的)
  Object.assign(userInfo, res)
  // 注意:Object.assign(userInfo, res)是对的,因为是在原有的reactive对象上添加/修改属性,Proxy能监听到
  // 千万不能写成userInfo = Object.assign({}, res),这又回到替换顶层变量的错误了!
})
watch(userInfo, (newVal) => console.log('userInfo变了:', newVal), { deep: true })
</script>

这里要提一下Vue3的一个改进:Vue3的Proxy可以直接监听对象的新增属性删除属性,不需要像Vue2那样用$set或者$delete了,这也是Object.assign能直接用在空reactive对象上的原因。

<script setup>
import { reactive, watch, onMounted } from 'vue'
// 正确写法1.2:用reactive包裹一个带data属性的容器(这种写法更通用,尤其适合有分页、加载状态等额外状态的情况)
const state = reactive({
  loading: false,
  userInfo: null // 或者空对象{},看需求
})
onMounted(async () => {
  state.loading = true
  const res = await new Promise(resolve => setTimeout(() => resolve({ id: 1, name: '接口返回的小明', age: 25 }), 1000))
  // 替换的是state的userInfo属性,不是state本身,Proxy能监听到
  state.userInfo = res
  state.loading = false
})
// 监听整个state也行,或者只监听userInfo
watch(() => state.userInfo, (newVal) => console.log('state.userInfo变了:', newVal), { deep: true, immediate: false })
</script>

这种写法用得最多,因为实际开发中一个组件不可能只有一个接口数据,通常还有loading、error、分页页码、每页条数这些状态,把它们都放在一个reactive的state容器里,代码看起来更整洁,维护起来也方便。

用ref的正确写法

其实用ref处理接口返回的引用类型数据(比如对象、数组),反而不容易踩替换顶层变量的坑——因为ref不管包的是基本类型还是引用类型,它的.value都是响应式的,替换整个.value的话,Vue3会自动给新的.value(如果是引用类型)重新套上reactive?不对不对,等下,咱们再仔细看一下Vue3的源码逻辑(这里就不用讲太细,只要记住结论就行):

  • 如果ref的初始值是基本类型(比如null、undefined、数字、字符串、布尔值),那么ref的.value直接是一个被“依赖追踪/触发更新”包装过的变量;
  • 如果ref的初始值是引用类型(比如对象、数组),或者后来给ref的.value赋值了引用类型,那么Vue3会自动调用reactive把这个引用类型包起来,作为ref的.value的值;
  • 不管什么时候替换ref的.value,哪怕是从引用类型换成基本类型,或者反过来,Vue3都会正确处理依赖追踪和触发更新。

所以用ref处理接口数据是最简单的,新手可以优先考虑:

<script setup>
import { ref, watch, onMounted } from 'vue'
// 正确写法2:直接用ref包裹null或者空对象/数组
const userInfo = ref(null)
onMounted(async () => {
  const res = await new Promise(resolve => setTimeout(() => resolve({ id: 1, name: '接口返回的小明', age: 25 }), 1000))
  // 直接替换整个userInfo.value,完全没问题!Vue3会自动处理
  userInfo.value = res
})
// 监听userInfo,不用加getter,直接传ref变量就行
// 注意:如果userInfo是引用类型,默认只监听.value的引用变化,要监听内部属性的话得加deep: true
watch(userInfo, (newVal) => console.log('userInfo变了:', newVal), { deep: true })
</script>

是不是简单多了?那为什么有时候用ref还是会遇到问题呢?别着急,咱们接下来讲第二个核心踩坑点。

核心踩坑2:watch的“监听对象选得不对”,或者“监听时机没选对”

刚才讲了,被监听的必须是响应式引用类型或者返回响应式的getter函数,那这里又有几个细分的小坑:

小坑2.1:监听reactive对象的属性时,直接传了属性名(没有用getter函数)

这个也是新手常犯的,咱们先看一段错误代码:

<script setup>
import { reactive, watch, onMounted } from 'vue'
const state = reactive({
  userInfo: {
    name: '初始小明'
  }
})
onMounted(async () => {
  const res = await new Promise(resolve => setTimeout(() => resolve({ name: '接口返回的小华' }), 1000))
  state.userInfo.name = res.name
})
// 错误写法!直接传state.userInfo.name,这只是一个基本类型的字符串(初始值是,接口回来也是),不是响应式引用,所以watch根本不会监听
watch(state.userInfo.name, (newVal) => console.log('name变了:', newVal))
</script>

那正确的写法是什么?如果要监听reactive对象的单个属性,不管这个属性是基本类型还是引用类型,都必须用返回这个属性的getter函数

<script setup>
import { reactive, watch, onMounted } from 'vue'
const state = reactive({
  userInfo: {
    name: '初始小明'
  }
})
onMounted(async () => {
  const res = await new Promise(resolve => setTimeout(() => resolve({ name: '接口返回的小华' }), 1000))
  state.userInfo.name = res.name
})
// 正确写法:用getter函数返回要监听的属性
watch(() => state.userInfo.name, (newVal) => console.log('name变了:', newVal))
</script>

那如果要监听reactive对象的多个属性呢?可以把getter函数放在一个数组里:

<script setup>
import { reactive, watch, onMounted } from 'vue'
const state = reactive({
  userInfo: {
    name: '初始小明',
    age: 20
  }
})
onMounted(async () => {
  const res = await new Promise(resolve => setTimeout(() => resolve({ name: '接口返回的小华', age: 25 }), 1000))
  state.userInfo.name = res.name
  state.userInfo.age = res.age
})
// 正确写法:多个getter函数放在数组里
watch([() => state.userInfo.name, () => state.userInfo.age], ([newName, newAge], [oldName, oldAge]) => {
  console.log('name从', oldName, '变到', newName)
  console.log('age从', oldAge, '变到', newAge)
})
</script>

小坑2.2:先在onMounted里赋值,再在下面定义watch(没有加immediate)

刚才咱们在讲响应式例子的时候就提到了这个小细节,咱们再结合onMounted的场景看一下:

<script setup>
import { ref, watch, onMounted } from 'vue'
const userInfo = ref(null)
// 注意:这里的onMounted回调是在组件挂载后才执行的,对吧?
// 那如果接口请求的速度特别快(比如mock数据没有加延迟,或者本地开发接口响应只有几毫秒),会不会在watch定义之前就执行完赋值了?
// 其实不会!因为Vue3的setup执行顺序是:先执行所有同步代码(包括定义ref、reactive、watch、computed这些),然后再执行生命周期钩子的回调(比如onMounted)
// 但是如果接口响应是异步的,哪怕只有1毫秒,也是在setup的同步代码执行完之后才会执行,对吧?
// 那为什么有时候会出现“接口回来赋值了,但watch没触发第一次”的情况?哦,不对,刚才的前提是同步代码执行完再执行异步,那如果接口是同步的?
// 不可能,接口请求都是异步的,哪怕是new Promise(resolve => resolve(data)),resolve也是在微任务队列里,要等同步代码执行完才会执行
// 那什么时候会需要加immediate?
// 当你希望watch在**定义的时候就立即执行一次**,比如组件刚挂载,哪怕接口还没回来,你也想拿到初始的userInfo(比如null)做一些处理,或者接口回来的第一次赋值也要触发watch(其实刚才的例子里如果接口是异步的,watch定义在同步代码里,接口回来赋值是异步的,第一次赋值也会触发watch呀?不对,等下,咱们再写个例子验证一下:
const count = ref(0)
onMounted(() => {
  // 同步代码里的赋值,在setup同步代码执行完之后才执行
  count.value = 1
})
// 这里的watch是在同步代码里定义的,监听count
watch(count, (newVal) => console.log('count变了:', newVal))
// 执行顺序:
// 1. 定义count = ref(0)
// 2. 定义onMounted回调(不执行)
// 3. 定义watch(count, ...)(依赖追踪count,设置回调)
// 4. setup同步代码执行完,开始执行微任务队列(没有的话跳过)
// 5. 组件挂载,执行onMounted回调,count.value = 1,触发watch,打印count变了:1
// 哦,那其实刚才说的“先赋值再watch”的场景在onMounted里根本不会出现?那为什么会有人遇到这种问题?
// 哦!哦对了!如果有人把watch的定义放在了onMounted的回调里面?
// 哦,对!那才是真正的“先赋值再watch”!咱们看一下:
<script setup>
import { ref, watch, onMounted } from 'vue'
const userInfo = ref(null)
// 错误的代码组织方式:把watch放在onMounted里,而且先赋值再定义watch
onMounted(async () => {
  const res = await new Promise(resolve => setTimeout(() => resolve({ id: 1 }), 1000))
  userInfo.value = res // 先赋值,此时还没定义watch,依赖追踪还没建立
  watch(userInfo, (newVal) => console.log('userInfo变了:', newVal)) // 定义watch的时候,userInfo已经是{id:1}了,但没有immediate,所以不会触发第一次
})
// 这段代码里,除非后面再修改userInfo.value,否则watch永远不会触发!
</script>

那解决这个问题的方法有两个:一个是把watch的定义放在setup的同步代码里(这也是官方推荐的做法,所有的响应式状态、计算属性、监听器都应该在setup的顶层定义,不要放在条件语句、循环语句或者生命周期钩子里面);另一个是如果必须把watch放在某个地方(比如条件满足才监听),那就要加immediate: true,让watch在定义的时候就立即执行一次,拿到最新的值:

<script setup>
import { ref, watch, onMounted, ref as shallowRef } from 'vue'
const shouldWatch = ref(false)
const userInfo = ref(null)
onMounted(async () => {
  const res = await new Promise(resolve => setTimeout(() => resolve({ id: 1 }), 1000))
  userInfo.value = res
  shouldWatch.value = true
})
// 比如只有shouldWatch为true的时候才监听,那可以用watchEffect或者加条件
// 这里用watch加条件的话,可能不太方便,用watchEffect更灵活,但今天咱们主要讲watch
// 如果必须用watch,而且放在某个动态条件里,记得加immediate
if (shouldWatch.value) {
  watch(userInfo, (newVal) => console.log('userInfo变了:', newVal), { immediate: true })
}
// 不过这种写法其实有问题,因为shouldWatch.value初始是false,if语句在同步代码里执行一次就完了,后面shouldWatch.value变成true也不会再执行if语句,所以更好的写法是用watchEffect:
// watchEffect(() => {
//   if (shouldWatch.value && userInfo.value) {
//     console.log('userInfo变了:', userInfo.value)
//   }
// })
</script>

小坑2.3:监听引用类型的ref/reactive时,没有加deep: true,只监听了引用变化

刚才咱们在讲ref的正确写法的时候提到过这个小坑,咱们再结合onMounted的场景看一下:

<script setup>
import { ref, watch, onMounted } from 'vue'
const userInfo = ref({ name: '初始小明' })
onMounted(async () => {
  const res = await new Promise(resolve => setTimeout(() => resolve('小华'), 1000))
  // 只修改内部属性,不修改.userInfo.value的引用地址
  userInfo.value.name = res
})
// 错误写法!没有加deep: true,watch只监听.userInfo.value的引用变化,内部属性修改不会触发
watch(userInfo, (newVal) => console.log('userInfo变了:', newVal))
</script>

那什么时候需要加deep: true呢?

  • 当你监听的是引用类型的ref,而且你需要监听它内部属性的变化,而不是整个引用的变化;
  • 当你监听的是reactive对象的数组属性,而且你需要监听数组内部元素的变化(比如push、pop、shift、unshift、splice这些方法,或者直接修改数组的某个索引值);
  • 当你监听的是返回引用类型的getter函数,而且你需要监听它内部的变化。

不过加deep: true也有一个小缺点:它会递归遍历整个引用类型的对象/数组,建立所有属性的依赖追踪,如果对象/数组特别大(比如有几千个属性的配置对象,或者几万条数据的列表),会有一定的性能损耗,所以如果不需要监听内部属性的变化,就不要加deep: true;如果只需要监听内部的某几个属性,就用刚才讲的“多个getter函数放在数组里”的方法,这样性能更好。

核心踩坑3:用了shallowRef、shallowReactive、markRaw这些API,但忘了它们的作用

Vue3为了优化性能,提供了几个“浅层次响应式”或者“标记非响应式”的API:shallowRef、shallowReactive、markRaw、toRaw,如果你不小心在接口数据处理的时候用了这些API,也会导致watch不触发,咱们简单说一下这些API的作用:

  • shallowRef:只有.value的变化是响应式的,.value内部的属性变化不是响应式的;
  • shallowReactive:只有第一层属性的变化是响应式的,深层属性的变化不是响应式的;
  • markRaw:标记一个对象,永远不会被Vue3转换成响应式的;
  • toRaw:获取响应式对象对应的原始普通对象,修改原始对象不会触发响应式更新。

咱们举两个例子看一下:

<script setup>
import { shallowRef, watch, onMounted } from 'vue'
// 错误示例:用shallowRef包裹接口数据,只修改内部属性不会触发watch
const userInfo = shallowRef({ name: '初始小明' })
onMounted(async () => {
  const res = await new Promise(resolve => setTimeout(() => resolve('小华'), 1000))
  userInfo.value.name = res // 只修改内部属性,shallowRef不监听,所以watch不会触发
  // 如果改成userInfo.value = { name: res },替换整个.value的引用,就会触发watch
})
watch(userInfo, (newVal) => console.log('userInfo变了:', newVal))
</script>
<script setup>
import { reactive, markRaw, watch, onMounted } from 'vue'
// 错误示例:把接口返回的对象用markRaw标记了,然后赋值给reactive的属性
const state = reactive({
  userInfo: null
})
onMounted(async () => {
  const res = await new Promise(resolve => setTimeout(() => resolve({ name: '接口返回的小明' }), 1000))
  const rawRes = markRaw(res) // 标记成非响应式
  state.userInfo = rawRes // 赋值给reactive的属性,但因为rawRes被markRaw标记了,所以state.userInfo不是响应式的,修改它的属性不会触发watch
})
watch(() => state.userInfo, (newVal) => console.log('userInfo变了:', newVal), { deep: true })
</script>

那什么时候用这些API呢?

  • 当你有一个特别大的配置对象,只需要监听它的第一层变化(比如切换整个配置对象),不需要监听内部属性的变化,就可以用shallowReactive;
  • 当你有一个第三方库的实例(比如axios的实例、echarts的实例),不需要转换成响应式的,就可以用markRaw标记,避免Vue3浪费性能去递归转换;
  • 当你需要修改响应式对象,但不想触发响应式更新(比如批量修改数据,最后再一次性赋值给响应式对象),就可以用toRaw获取原始对象,修改完再赋值回去。

但新手刚开始学的时候,尽量不要用这些API,等对Vue3的响应式系统完全熟悉了,再根据性能需求来选择。

除了这些坑,onMounted和watch还有哪些配合使用的常见场景?

刚才咱们讲了那么多踩坑点,现在咱们讲一些onMounted和watch配合使用的实际开发场景,帮助大家更好地理解:

场景1:页面刚挂载就获取数据,获取到数据后初始化echarts图表

这个是最常见的场景之一:

<script setup>
import { ref, watch, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
// 定义echarts实例的ref
const chartRef = ref(null)
// 定义图表数据的ref
const chartData = ref(null)
// 定义获取数据的函数
const getChartData = async () => {
  const res = await new Promise(resolve => setTimeout(() => resolve([10, 20, 30, 40, 50]), 1000))
  chartData.value = res
}
// 定义初始化图表的函数
const initChart = () => {
  if (!chartRef.value) return
  const chartInstance = echarts.init(chartRef.value)
  const option = {
    xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'] },
    yAxis: { type: 'value' },
    series: [{ type: 'bar', data: chartData.value }]
  }
  chartInstance.setOption(option)
  // 监听窗口大小变化,自动调整图表大小
  window.addEventListener('resize', () => chartInstance.resize())
  // 组件卸载时销毁实例,避免内存泄漏
  onUnmounted(() => {
    window.removeEventListener('resize', () => chartInstance.resize())
    chartInstance.dispose()
  })
}
// 组件刚挂载时获取数据
onMounted(() => {
  getChartData()
})
// 监听chartData,获取到数据后,等DOM更新完(因为chartRef可能是通过v-if或者条件渲染出来的),再初始化图表
watch(chartData, (newVal) => {
  if (newVal) {
    nextTick(() => {
      initChart()
    })
  }
})
</script>
<template>
  <div ref="chartRef" style="width: 600px; height: 400px;"></div>
</template>

这里要注意两个点:一个是用了nextTick,因为chartData.value赋值后,Vue3会先更新虚拟DOM,然后再更新真实DOM,chartRef.value可能还没来得及挂载到真实DOM上,所以要等nextTick之后再获取chartRef.value初始化图表;另一个是在onUnmounted里销毁echarts实例,移除窗口大小变化的监听,避免内存泄漏。

场景2:页面刚挂载就获取列表数据,监听分页页码或每页条数的变化,重新获取数据

这个也是非常常见的后台管理系统列表页的场景:

<script setup>
import { ref, reactive, watch, onMounted } from 'vue'
// 定义分页状态
const pagination = reactive({
  page: 1,
  pageSize: 10,
  total: 0
})
// 定义列表数据
const listData = ref([])
// 定义loading状态
const loading = ref(false)
// 定义获取列表数据的函数
const getListData = async () => {
  loading.value = true
  try {
    // 模拟接口请求,带分页参数
    const res = await new Promise(resolve => {
      setTimeout(() => {
        const start = (pagination.page - 1) * pagination.pageSize
        const end = start + pagination.pageSize
        const data = []
        for (let i = start; i < end; i++) {
          data.push({ id: i + 1, name: `第${i + 1}条数据` })
        }
        resolve({ data, total: 100 })
      }, 1000)
    })
    listData.value = res.data
    pagination.total = res.total
  } catch (error) {
    console.error('获取列表数据失败:', error)
  } finally {
    loading.value = false
  }
}
// 组件刚挂载时获取第一页数据
onMounted(() => {
  getListData()
})
// 监听分页页码或每页条数的变化,重新获取数据
watch([() => pagination.page, () => pagination.pageSize], () => {
  // 变化的时候重置到第一页?不对,只有pageSize变化的时候才需要重置到第一页,page变化的时候不需要
  // 哦,对,刚才的数组里两个变量变化都会触发watch,所以要区分一下
  // 或者分开写两个watch
  watch(() => pagination.page, () => {
    getListData()
  })
  watch(() => pagination.pageSize, () => {
    pagination.page = 1
    getListData()
  })
  // 刚才的写法不对,外层的watch([])和内层的两个watch会重复执行,所以直接分开写两个watch就行
})
// 正确的分开写两个watch:
watch(() => pagination.page, () => {
  getListData()
})
watch(() => pagination.pageSize, () => {
  pagination.page = 1
  getListData()
})
</script>

这里要注意的是pageSize变化的时候要把page重置为1,否则可能会出现数据越界的情况。

Vue3中onMounted获取数据后watch要触发的关键要点

今天咱们讲了这么多,现在来总结一下关键要点,方便大家记忆:

  1. 确保被监听的是响应式数据:优先用ref处理接口返回的数据,不容易踩替换顶层变量的坑;如果用reactive,不要替换顶层变量,只能替换它的属性或者用Object.assign在原对象上添加/修改属性;
  2. 正确选择watch的监听对象:监听reactive对象的单个/多个属性时,必须用返回这些属性的getter函数;
  3. 注意watch的监听时机和参数:把watch的定义放在setup的顶层,不要放在条件语句、循环语句或者生命周期钩子里面;如果需要监听引用类型内部属性的变化,加deep: true;如果需要watch在定义的时候就立即执行一次,加immediate: true;
  4. 避免使用不熟悉的性能优化API:新手刚开始学的时候,尽量不要用shallowRef、shallowReactive、markRaw这些API,等熟悉了响应式系统再根据需求选择;
  5. 配合nextTick和onUnmounted使用:如果监听数据后需要操作DOM,记得用nextTick;如果有第三方库的实例或者事件监听,记得在onUnmounted里清理,避免内存泄漏。

只要掌握了这些要点,以后遇到onMounted获取数据后watch不触发的问题,就能很快定位并解决了,其实Vue3的响应式系统比Vue2更强大、更灵活,只要多写多练,多踩几个坑,就能很快上手了。

版权声明

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

热门