vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue3数组不能实时DOM更新

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

作者:阿姨给我倒一杯卡布奇诺

在写大文件上传时,碰到关于 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更新的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文