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更新的资料请关注脚本之家其它相关文章!

相关文章

  • Vue-cli3简单使用(图文步骤)

    Vue-cli3简单使用(图文步骤)

    这篇文章主要介绍了Vue-cli3简单使用(图文步骤),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-04-04
  • 使用Vue3优雅地实现表格拖动排序

    使用Vue3优雅地实现表格拖动排序

    在 Vue.js 中主要通过第三方库实现表格拖动排序功能,其中最常用的库是 SortableJS,下面我们就来看看如何使用SortableJS实现表格拖动排序吧
    2025-01-01
  • 茶余饭后聊聊Vue3.0响应式数据那些事儿

    茶余饭后聊聊Vue3.0响应式数据那些事儿

    这篇文章主要介绍了茶余饭后聊聊Vue3.0响应式数据那些事儿,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-10-10
  • vue实现select下拉显示隐藏功能

    vue实现select下拉显示隐藏功能

    这篇文章主要介绍了vue实现select下拉显示隐藏功能,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-09-09
  • Vite打包时去除console的方法实现

    Vite打包时去除console的方法实现

    Vite打包项目时,需要去除开发时加入的console、debugger调试信息,本文主要介绍了Vite打包时去除console的方法实现,具有一定的参考价值,感兴趣的可以了解一下
    2024-08-08
  • vue轮播图插件vue-awesome-swiper的使用代码实例

    vue轮播图插件vue-awesome-swiper的使用代码实例

    本篇文章主要介绍了vue轮播图插件vue-awesome-swiper的使用代码实例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-07-07
  • Vue router配置与使用分析讲解

    Vue router配置与使用分析讲解

    第一次写Vue项目,要用到router.js,看了一下官方文档,还是很懵逼,不知道怎么配置,又去看视频查资料,最后终于搞定了。话不多说,先上代码,我再讲一些要注意的细节
    2022-12-12
  • vue+elementUI组件table实现前端分页功能

    vue+elementUI组件table实现前端分页功能

    这篇文章主要为大家详细介绍了vue+elementUI组件table实现前端分页功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-12-12
  • Vue的模板语法以及实战案例

    Vue的模板语法以及实战案例

    Vue使用了基于HTML的模板语法,允许开发者声明式地将DOM绑定至底层Vue实例的数据,下面这篇文章主要给大家介绍了关于Vue的模板语法以及案例的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-06-06
  • vuex使用及持久化方式

    vuex使用及持久化方式

    这篇文章主要介绍了vuex使用及持久化方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-08-08

最新评论