Vue3響應(yīng)式對(duì)象數(shù)組不能實(shí)時(shí)DOM更新問題解決辦法
前言
之所以寫該文章是在自己寫大文件上傳時(shí),碰到關(guān)于 vue2
跟 vue3
對(duì)在循環(huán)中使用異步,并動(dòng)態(tài)把普通對(duì)象添加進(jìn)響應(yīng)式數(shù)據(jù),在異步前后修改該普通對(duì)象的某個(gè)屬性,導(dǎo)致 vue2 跟 vue3 的視圖更新不一致,引發(fā)一系列的思考。
forEach 中使用異步
forEach() 期望的是一個(gè)同步函數(shù),它不會(huì)等待 Promise 兌現(xiàn)。在使用 Promise(或異步函數(shù))作為 forEach 回調(diào)時(shí),請(qǐng)確保你意識(shí)到這一點(diǎn)可能帶來的影響。
以上解釋是 MDN 關(guān)于對(duì) forEach 的部分解釋,這里要注意的是,在 forEach 中使用異步是不會(huì)等待異步而暫停。所以如果不了解的小伙伴要注意一下,那就讓我們做個(gè)測(cè)試。
我們先定義一個(gè)異步回調(diào)函數(shù):
// 延時(shí)回調(diào)函數(shù) const asyncFunc = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('執(zhí)行延遲:', new Date()) resolve() }, 1000) }) }
再定義一個(gè)關(guān)于 forEach 的函數(shù)并執(zhí)行
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()
讓我們看看最終的打印結(jié)果
根據(jù)輸出結(jié)果可以看到:有五次循環(huán),但五次循環(huán)基本是按順序同步執(zhí)行,在每次循環(huán)遇到異步后,并不會(huì)阻塞 forEach 外部代碼執(zhí)行,而是把每次循環(huán)單獨(dú)處理異步,在內(nèi)部等待異步完成后處理邏輯。
for 中使用異步
而 for 循環(huán)是會(huì)阻塞下一個(gè)循環(huán)并等待本次異步完后再處理下一個(gè)循環(huán),等待全部循環(huán)完后再執(zhí)行 for 循環(huán)下面的代碼。
那讓我們?cè)衮?yàn)證以上的 for 循環(huán)異步理論是否正確:
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()
根據(jù)控制臺(tái)輸出可以看到,通過打印的 i 跟時(shí)間可以判斷:先執(zhí)行完當(dāng)前循環(huán)的異步后再執(zhí)行一下循環(huán),且等所有循環(huán)處理完再執(zhí)行 for 循環(huán)外部的代碼
需求
因?yàn)樵诖笪募蟼髦猩婕暗轿募蟼鳡顟B(tài)的更變,現(xiàn)在需求是:需要在循環(huán)中把一個(gè)普通對(duì)象 push 到響應(yīng)式數(shù)組中,并修改該對(duì)象的 state 屬性,在等待一個(gè)異步回調(diào)后,再去修改 state 值,并要在頁面視圖中展現(xiàn)改變。
vue2 代碼實(shí)現(xiàn)
在模板代碼中,直接在視圖展示全部數(shù)組,并用 v-for
遍歷
<template> <div> 數(shù)組數(shù)據(jù): <div> {{ testArr }} </div> <div style="margin-top: 50px"> <div v-for="item in testArr" :key="item.id"> {{ item.state }} </div> </div> </div> </template>
在script 中,定義響應(yīng)式數(shù)組,以及一個(gè)異步回調(diào)函數(shù),并分別定義用 for 循環(huán)跟 forEach 處理異步修改狀態(tài)的方法,并在 mounted 生命周期里分別執(zhí)行這兩個(gè)方法
<script> export default { data() { return { testArr: [], } }, mounted() { this.forFunc() // this.forEachFunc() }, methods: { asyncFunc() { return new Promise((resolve, reject) => { setTimeout(() => { console.log('執(zhí)行延遲:', new Date()) resolve('延遲成功') }, 1000) }) }, // for循環(huán) 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循環(huán) 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 循環(huán)效果
可以看到刷新頁面后,在一秒延遲后數(shù)組內(nèi)所有對(duì)象的 state 屬性同步變化
2. for 循環(huán)效果展示
可以看到在 Vue2 中 DOM 視圖是正常更新,且用 for 循環(huán)是先執(zhí)行完當(dāng)前循環(huán)的異步后再執(zhí)行一下循環(huán),且等所有循環(huán)處理完再執(zhí)行 for 循環(huán)外部的代碼
3. 小結(jié)
在 vue2 中在循環(huán)中使用異步,并動(dòng)態(tài)把普通對(duì)象添加進(jìn)響應(yīng)式數(shù)組,在異步前后修改該普通對(duì)象的某個(gè)屬性,修改的是該數(shù)組具體對(duì)象某一屬性,且視圖能正常更新。
vue3 代碼實(shí)現(xiàn)
模板代碼中,直接在視圖展示全部數(shù)組,并用 v-for 遍歷
<template> <div> 數(shù)組數(shù)據(jù): <div> {{ testArr }} </div> <div style="margin-top: 50px"> <div v-for="item in testArr" :key="item.id"> {{ item.state }} </div> </div> </div> </template>
在script 中,定義響應(yīng)式數(shù)組,以及一個(gè)異步回調(diào)函數(shù),并分別定義用 for 循環(huán)跟 forEach 處理異步修改狀態(tài)的方法,并在 mounted 生命周期里分別執(zhí)行這兩個(gè)方法
<script setup> import { ref, onMounted, reactive } from 'vue' const testArr = ref([]) // 延時(shí)回調(diào) const asyncFunc = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('執(zhí)行延遲:', new Date()) resolve() }, 1000) }) } // for-正常push進(jìn)去后直接修改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進(jìn)去后直接修改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 循環(huán)效果
!可以看到,在異步后面的 state 修改并沒有生效,但是為什么在控制臺(tái)console.log的值卻又改變了?
關(guān)于console.log
這里為什么要說 console.log 呢,可能很多人沒注意在控制臺(tái)用 console 打印對(duì)象時(shí),是會(huì)隨著值變化也不斷更新的。所以你在最后中看到的值并不是當(dāng)時(shí)打印的值,要注意!
以下是 MDN 的部分解釋
所以這就是解釋了以上現(xiàn)象,為什么最終在打印的數(shù)組,是改變后的。但為什么視圖沒有更新呢?讓我們?cè)偈褂?for 循環(huán)+ await 測(cè)試看看會(huì)發(fā)生什么
2. for 循環(huán)效果
onMounted(() => { // forFunc() forEachFunc() })
在頁面中可以看到,for 循環(huán)是按順序異步更新的,但是最后一個(gè) item 在視圖并沒有更新,控制臺(tái)打印的最終值確實(shí)更新了的
那到底是什么原因呢?初步判斷:vue3 的響應(yīng)式監(jiān)聽的是代理對(duì)象,因?yàn)樵谘h(huán)中使用異步,對(duì)普通對(duì)象的修改可能不能及時(shí)監(jiān)聽到,而 vue2 生效的原因是在于它本身就是在原對(duì)象的 get set 上操作的。
至于為什么 for 循環(huán)+異步會(huì)生效,而最后一個(gè)未更新,因?yàn)樵诿總€(gè) item 循環(huán)中,push 觸發(fā)了數(shù)組改變,從而導(dǎo)致視圖更新,但在最后循環(huán)中,在 await 后面并沒有更改數(shù)組。
那就讓我們多做幾個(gè)實(shí)驗(yàn)測(cè)試一下
3. 用reactive創(chuàng)建對(duì)象
// for-用reactive創(chuàng)建對(duì)象 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創(chuàng)建對(duì)象 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') }
那讓我們來分別看一下這兩個(gè)函數(shù)執(zhí)行的效果
for 循環(huán):
可以看到用 reactive 創(chuàng)建的代理對(duì)象會(huì)被Vue跟蹤到,且視圖進(jìn)行了實(shí)時(shí)更新
forEach 循環(huán):
最終結(jié)果也是能正常更新
4. 直接取數(shù)組下標(biāo)對(duì)象修改
直接通過 testArr.value[i].state = 'after的name'
去修改。
// for-直接取數(shù)組下標(biāo)對(duì)象修改 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-直接取數(shù)組下標(biāo)對(duì)象修改 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 循環(huán):
forEach 循環(huán):
通過取數(shù)組下標(biāo)對(duì)象修改是能實(shí)時(shí)更新的,因?yàn)橄喈?dāng)于直接修改響應(yīng)式對(duì)象的某一個(gè)值,這樣Vue3也能正常監(jiān)聽到并視圖更新
5. 重新賦值對(duì)象引用地址
通過 obj = testArr.value[i]
方式去修改。
// for-重新賦值對(duì)象引用 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-重新賦值對(duì)象引用 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 循環(huán):
forEach 循環(huán):
通過引用響應(yīng)式數(shù)據(jù)對(duì)象地址是能實(shí)時(shí)更新的,同樣的效果,這是因?yàn)閮蓚€(gè)對(duì)象引用的是同一個(gè)對(duì)象地址,從而實(shí)現(xiàn)被
Vue3
追蹤到并進(jìn)行視圖更新
小結(jié)
根據(jù)這幾種測(cè)試可以得出一個(gè)結(jié)論:在vue3中,若是在循環(huán)中并動(dòng)態(tài)把普通對(duì)象添加(push)進(jìn)響應(yīng)式數(shù)據(jù),在異步前后修改直接該普通對(duì)象的某個(gè)屬性,不一定被Vue追蹤到這個(gè)變化,并在需要時(shí)更新 DOM。
所以如果想要實(shí)現(xiàn)DOM實(shí)時(shí)更新,應(yīng)該 1.用 reactive
去創(chuàng)建該對(duì)象;2.直接使用該數(shù)組指定下標(biāo)的對(duì)象修改屬性;3.使用對(duì)象賦值(=
)的方式直接引用響應(yīng)式數(shù)據(jù)的地址。
溫馨提示:就算用Vue2的寫法直接放在Vue3版本的項(xiàng)目中,最終效果也是同Vue3寫法一樣,無論是vite創(chuàng)建還是vue-cli創(chuàng)建的Vue3項(xiàng)目。
以上就是Vue3響應(yīng)式對(duì)象數(shù)組不能實(shí)時(shí)DOM更新問題解決辦法的詳細(xì)內(nèi)容,更多關(guān)于Vue3數(shù)組不能實(shí)時(shí)DOM更新的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用Vue3優(yōu)雅地實(shí)現(xiàn)表格拖動(dòng)排序
在?Vue.js?中主要通過第三方庫實(shí)現(xiàn)表格拖動(dòng)排序功能,其中最常用的庫是?SortableJS,下面我們就來看看如何使用SortableJS實(shí)現(xiàn)表格拖動(dòng)排序吧2025-01-01茶余飯后聊聊Vue3.0響應(yīng)式數(shù)據(jù)那些事兒
這篇文章主要介紹了茶余飯后聊聊Vue3.0響應(yīng)式數(shù)據(jù)那些事兒,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10vue實(shí)現(xiàn)select下拉顯示隱藏功能
這篇文章主要介紹了vue實(shí)現(xiàn)select下拉顯示隱藏功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09Vite打包時(shí)去除console的方法實(shí)現(xiàn)
Vite打包項(xiàng)目時(shí),需要去除開發(fā)時(shí)加入的console、debugger調(diào)試信息,本文主要介紹了Vite打包時(shí)去除console的方法實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-08-08vue輪播圖插件vue-awesome-swiper的使用代碼實(shí)例
本篇文章主要介紹了vue輪播圖插件vue-awesome-swiper的使用代碼實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07vue+elementUI組件table實(shí)現(xiàn)前端分頁功能
這篇文章主要為大家詳細(xì)介紹了vue+elementUI組件table實(shí)現(xiàn)前端分頁功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12