Vue3响应式对象数组不能实时DOM更新问题解决办法

 更新时间:2024年07月11日 09:02:43   作者:阿姨给我倒一杯卡布奇诺  
在写大文件上传时,碰到关于 vue2 跟 vue3 对在循环中使用异步,并动态把普通对象添加进响应式数据,在异步前后修改该普通对象的某个属性,导致 vue2 跟 vue3 的视图更新不一致,引发一系列的思考,所以本文介绍了Vue3响应式对象数组不能实时DOM更新问题解决办法

前言

之所以写该文章是在自己写大文件上传时,碰到关于 vue2vue3在循环中使用异步,并动态把普通对象添加进响应式数据,在异步前后修改该普通对象的某个属性,导致 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更新的资料请关注脚本之家其它相关文章!

相关文章

  • vuecli3.0脚手架搭建及不同的打包环境配置vue.config.js的详细过程

    vuecli3.0脚手架搭建及不同的打包环境配置vue.config.js的详细过程

    这篇文章主要介绍了vuecli3.0脚手架搭建及不同的打包环境配置vue.config.js的详细过程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-01-01
  • vue.js Router中嵌套路由的实用示例

    vue.js Router中嵌套路由的实用示例

    这篇文章主要给大家介绍了关于vue.js Router中嵌套路由的相关资料,所谓嵌套路由就是路由里面嵌套他的子路由,文章通过示例代码介绍的非常详细,需要的朋友可以参考下
    2021-06-06
  • 解决el-select数据量过大的3种方案

    解决el-select数据量过大的3种方案

    最近做完一个小的后台管理系统,快上线了,发现一个问题,有2个select的选项框线上的数据量是1w+,而测试环境都是几百的,所以导致页面直接卡住了,本文给大家总结了3种方法,需要的朋友可以参考下
    2023-09-09
  • Vue中在data里面调用method方法的实现

    Vue中在data里面调用method方法的实现

    这篇文章主要介绍了Vue中在data里面调用method方法的实现,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-06-06
  • web项目开发VUE的混入与继承原理

    web项目开发VUE的混入与继承原理

    这篇文章主要介绍了web项目开发中VUE的混入与继承原理,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2021-09-09
  • Vue中使用webpack别名的方法实例详解

    Vue中使用webpack别名的方法实例详解

    本文通过实例给大家介绍了Vue中使用webpack别名的方法,非常不错,具体一定的参考借鉴价值,需要的朋友可以参考下
    2018-06-06
  • 详解Vue中的基本语法和常用指令

    详解Vue中的基本语法和常用指令

    Vue.js 是一套构建用户界面的框架,**只关注视图层**,它不仅易于上手,还便于与第三方库或既有项目整合。这篇文章主要介绍了vue 的基本语法和常用指令,需要的朋友可以参考下
    2019-07-07
  • vue学习笔记之slot插槽基本用法实例分析

    vue学习笔记之slot插槽基本用法实例分析

    这篇文章主要介绍了vue学习笔记之slot插槽基本用法,结合实例形式分析了vue slot插槽基本使用方法与操作注意事项,需要的朋友可以参考下
    2020-02-02
  • vue使用天地图、openlayers实现多个底图叠加显示效果

    vue使用天地图、openlayers实现多个底图叠加显示效果

    这篇文章主要介绍了vue使用天地图、openlayers实现多个底图叠加显示,根据返回的经纬度列表通过天地图、openlayers实现底图添加,本文通过示例代码给大家介绍的非常详细,需要的朋友参考下吧
    2022-04-04
  • 详解vue路由篇(动态路由、路由嵌套)

    详解vue路由篇(动态路由、路由嵌套)

    这篇文章主要介绍了详解vue路由篇(动态路由、路由嵌套),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-01-01

最新评论