Vue3响应式对象数组不能实时DOM更新问题解决办法
作者:阿姨给我倒一杯卡布奇诺
前言
之所以写该文章是在自己写大文件上传时,碰到关于 vue2
跟 vue3
对在循环中使用异步,并动态把普通对象添加进响应式数据,在异步前后修改该普通对象的某个属性,导致 vue2 跟 vue3 的视图更新不一致,引发一系列的思考。
forEach 中使用异步
forEach() 期望的是一个同步函数,它不会等待 Promise 兑现。在使用 Promise(或异步函数)作为 forEach 回调时,请确保你意识到这一点可能带来的影响。
以上解释是 MDN 关于对 forEach 的部分解释,这里要注意的是,在 forEach 中使用异步是不会等待异步而暂停。所以如果不了解的小伙伴要注意一下,那就让我们做个测试。
我们先定义一个异步回调函数:
// 延时回调函数 const asyncFunc = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('执行延迟:', new Date()) resolve() }, 1000) }) }
再定义一个关于 forEach 的函数并执行
const forEachFunc = () => { let arr = new Array(5).fill({ test: 'test' }) arr.forEach(async (item, i) => { console.log(`异步前${i}:`,new Date()) await asyncFunc() console.log(`异步后${i}:`,new Date()) }) console.log('forEach外部:',new Date()) } forEachFunc()
让我们看看最终的打印结果
根据输出结果可以看到:有五次循环,但五次循环基本是按顺序同步执行,在每次循环遇到异步后,并不会阻塞 forEach 外部代码执行,而是把每次循环单独处理异步,在内部等待异步完成后处理逻辑。
for 中使用异步
而 for 循环是会阻塞下一个循环并等待本次异步完后再处理下一个循环,等待全部循环完后再执行 for 循环下面的代码。
那让我们再验证以上的 for 循环异步理论是否正确:
const forFunc = async () => { let arr = new Array(5).fill({ test: 'test' }) for (let i = 0; i < arr.length; i++) { console.log(`异步前${i}:`, new Date()) await asyncFunc() console.log(`异步后${i}:`, new Date()) } console.log('for外部:', new Date()) } forFunc()
根据控制台输出可以看到,通过打印的 i 跟时间可以判断:先执行完当前循环的异步后再执行一下循环,且等所有循环处理完再执行 for 循环外部的代码
需求
因为在大文件上传中涉及到文件上传状态的更变,现在需求是:需要在循环中把一个普通对象 push 到响应式数组中,并修改该对象的 state 属性,在等待一个异步回调后,再去修改 state 值,并要在页面视图中展现改变。
vue2 代码实现
在模板代码中,直接在视图展示全部数组,并用 v-for
遍历
<template> <div> 数组数据: <div> {{ testArr }} </div> <div style="margin-top: 50px"> <div v-for="item in testArr" :key="item.id"> {{ item.state }} </div> </div> </div> </template>
在script 中,定义响应式数组,以及一个异步回调函数,并分别定义用 for 循环跟 forEach 处理异步修改状态的方法,并在 mounted 生命周期里分别执行这两个方法
<script> export default { data() { return { testArr: [], } }, mounted() { this.forFunc() // this.forEachFunc() }, methods: { asyncFunc() { return new Promise((resolve, reject) => { setTimeout(() => { console.log('执行延迟:', new Date()) resolve('延迟成功') }, 1000) }) }, // for循环 async forFunc() { let arr = new Array(5).fill({ test: 'test' }) for (let i = 0; i < arr.length; i++) { let obj = { id: i, state: 'state' + i, } this.testArr.push(obj) obj.state = 'before前的name' await this.asyncFunc() obj.state = 'after后的name' } console.log(this.testArr, 'this.testArr') }, // forEach循环 forEachFunc() { let arr = new Array(5).fill({ test: 'test' }) arr.forEach(async (item, i) => { let obj = { id: i, state: 'state' + i, } this.testArr.push(obj) obj.state = 'before前的name' await this.asyncFunc() obj.state = 'after的name' }) console.log(this.testArr, 'this.testArr') }, }, } </script>
1. forEach 循环效果
可以看到刷新页面后,在一秒延迟后数组内所有对象的 state 属性同步变化
2. for 循环效果展示
可以看到在 Vue2 中 DOM 视图是正常更新,且用 for 循环是先执行完当前循环的异步后再执行一下循环,且等所有循环处理完再执行 for 循环外部的代码
3. 小结
在 vue2 中在循环中使用异步,并动态把普通对象添加进响应式数组,在异步前后修改该普通对象的某个属性,修改的是该数组具体对象某一属性,且视图能正常更新。
vue3 代码实现
模板代码中,直接在视图展示全部数组,并用 v-for 遍历
<template> <div> 数组数据: <div> {{ testArr }} </div> <div style="margin-top: 50px"> <div v-for="item in testArr" :key="item.id"> {{ item.state }} </div> </div> </div> </template>
在script 中,定义响应式数组,以及一个异步回调函数,并分别定义用 for 循环跟 forEach 处理异步修改状态的方法,并在 mounted 生命周期里分别执行这两个方法
<script setup> import { ref, onMounted, reactive } from 'vue' const testArr = ref([]) // 延时回调 const asyncFunc = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('执行延迟:', new Date()) resolve() }, 1000) }) } // for-正常push进去后直接修改obj const forFunc = async () => { let arr = new Array(5).fill({ test: 'test' }) for (let i = 0; i < arr.length; i++) { let obj = { id: i, state: 'state' + i, } testArr.value.push(obj) obj.state = 'before前的name' await asyncFunc() obj.state = 'after的name' } console.log(testArr.value, 'testArr.value') } // forEach-正常push进去后直接修改obj const forEachFunc = () => { let arr = new Array(5).fill({ test: 'test' }) arr.forEach(async (item, i) => { let obj = { id: i, state: 'state' + i, } testArr.value.push(obj) obj.state = 'before前的name' await asyncFunc() obj.state = 'after的name' }) console.log(testArr.value, 'testArr.value') } onMounted(() => { // forFunc() forEachFunc() }) </script>
1. forEach 循环效果
!可以看到,在异步后面的 state 修改并没有生效,但是为什么在控制台console.log的值却又改变了?
关于console.log
这里为什么要说 console.log 呢,可能很多人没注意在控制台用 console 打印对象时,是会随着值变化也不断更新的。所以你在最后中看到的值并不是当时打印的值,要注意!
以下是 MDN 的部分解释
所以这就是解释了以上现象,为什么最终在打印的数组,是改变后的。但为什么视图没有更新呢?让我们再使用 for 循环+ await 测试看看会发生什么
2. for 循环效果
onMounted(() => { // forFunc() forEachFunc() })
在页面中可以看到,for 循环是按顺序异步更新的,但是最后一个 item 在视图并没有更新,控制台打印的最终值确实更新了的
那到底是什么原因呢?初步判断:vue3 的响应式监听的是代理对象,因为在循环中使用异步,对普通对象的修改可能不能及时监听到,而 vue2 生效的原因是在于它本身就是在原对象的 get set 上操作的。
至于为什么 for 循环+异步会生效,而最后一个未更新,因为在每个 item 循环中,push 触发了数组改变,从而导致视图更新,但在最后循环中,在 await 后面并没有更改数组。
那就让我们多做几个实验测试一下
3. 用reactive创建对象
// for-用reactive创建对象 const forFunc2 = async () => { let arr = new Array(5).fill({ test: 'test' }) for (let i = 0; i < arr.length; i++) { let obj = reactive({ id: i, state: 'state' + i, }) testArr.value.push(obj) obj.state = 'before前的name' await asyncFunc() obj.state = 'after的name' } console.log(testArr.value, 'testArr.value') } // forEach-用reactive创建对象 const forEachFunc2 = () => { let arr = new Array(5).fill({ test: 'test' }) arr.forEach(async (item, i) => { let obj = reactive({ id: i, state: 'state' + i, }) testArr.value.push(obj) obj.state = 'before前的name' await asyncFunc() obj.state = 'after的name' }) console.log(testArr.value, 'testArr.value') }
那让我们来分别看一下这两个函数执行的效果
for 循环:
可以看到用 reactive 创建的代理对象会被Vue跟踪到,且视图进行了实时更新
forEach 循环:
最终结果也是能正常更新
4. 直接取数组下标对象修改
直接通过 testArr.value[i].state = 'after的name'
去修改。
// for-直接取数组下标对象修改 const forFunc3 = async () => { let arr = new Array(5).fill({ test: 'test' }) for (let i = 0; i < arr.length; i++) { let obj = reactive({ id: i, state: 'state' + i, }) testArr.value.push(obj) testArr.value[i].state = 'before前的name' await asyncFunc() testArr.value[i].state = 'after的name' } console.log(testArr.value, 'testArr.value') } // forEach-直接取数组下标对象修改 const forEachFunc3 = () => { let arr = new Array(5).fill({ test: 'test' }) arr.forEach(async (item, i) => { let obj = { id: i, state: 'state' + i, } testArr.value.push(obj) testArr.value[i].state = 'before前的name' await asyncFunc() testArr.value[i].state = 'after的name' }) console.log(testArr.value, 'testArr.value') }
for 循环:
forEach 循环:
通过取数组下标对象修改是能实时更新的,因为相当于直接修改响应式对象的某一个值,这样Vue3也能正常监听到并视图更新
5. 重新赋值对象引用地址
通过 obj = testArr.value[i]
方式去修改。
// for-重新赋值对象引用 const forFunc4 = async () => { let arr = new Array(5).fill({ test: 'test' }) for (let i = 0; i < arr.length; i++) { let obj = reactive({ id: i, state: 'state' + i, }) testArr.value.push(obj) obj = testArr.value[i] obj.state = 'before前的name' await asyncFunc() obj.state = 'after的name' } console.log(testArr.value, 'testArr.value') } // forEach-重新赋值对象引用 const forEachFunc4 = () => { let arr = new Array(5).fill({ test: 'test' }) arr.forEach(async (item, i) => { let obj = { id: i, state: 'state' + i, } testArr.value.push(obj) obj = testArr.value[i] obj.state = 'before前的name' await asyncFunc() obj.state = 'after的name' }) console.log(testArr.value, 'testArr.value') }
for 循环:
forEach 循环:
通过引用响应式数据对象地址是能实时更新的,同样的效果,这是因为两个对象引用的是同一个对象地址,从而实现被
Vue3
追踪到并进行视图更新
小结
根据这几种测试可以得出一个结论:在vue3中,若是在循环中并动态把普通对象添加(push)进响应式数据,在异步前后修改直接该普通对象的某个属性,不一定被Vue追踪到这个变化,并在需要时更新 DOM。
所以如果想要实现DOM实时更新,应该 1.用 reactive
去创建该对象;2.直接使用该数组指定下标的对象修改属性;3.使用对象赋值(=
)的方式直接引用响应式数据的地址。
温馨提示:就算用Vue2的写法直接放在Vue3版本的项目中,最终效果也是同Vue3写法一样,无论是vite创建还是vue-cli创建的Vue3项目。
以上就是Vue3响应式对象数组不能实时DOM更新问题解决办法的详细内容,更多关于Vue3数组不能实时DOM更新的资料请关注脚本之家其它相关文章!