Vue中异步请求更新data导致数据变为非响应式的问题

问题

最近在项目遇到一个棘手的问题,看了1天才发现是异步与Vue的数据更新机制导致了结果与预期不一致。

最开始的代码流程是这样:

  1. 通过一个请求拿到一组数据data1,包括下一个请求的关键字
  2. 在第一个请求的回掉函数中使用for循环利用上一个数组的关键字发送多个子请求得到对应的datan,并在每个子请求的回调中对data1和datan进行对比,然后给data1添加一个字段key
  3. 在第一个请求的回调中把修改后的data1赋给Vue实例的data中

产生的问题

在Vue实例中的data1对象中,原来的数据都是响应式的,但是我后来添加的数据并不是响应式的,导致DOM不更新。

解决过程

产生问题后我先后大概经过了3次修改来解决这个问题:

思路一:data转换为响应式的时间

首先我从响应式这个角度去分析,Vue是在哪个生命周期函数内将data挂载到Vue实例中的(因该是在beforeCreate与created之间),而我做这些操作是在mounted,所以此时data因该已经被挂载到了Vue实例中,后来我又在思考JS中存在变量提升,是不是Vue中也存在,即是否在mounted中存在譬如this.xxx = yyy这样的语句会被提升到其他语句之前执行,yyy的内存地址提前就会被写入到this.xxx的位置,然后在这个内存地址被赋值时就将其变为响应式,所以第一次data1被赋值为一个数组时他是响应式的,但是后面对这个内存地址的变量进行的操作是为数组内的对象进行赋值this.data1[i].key = val,这样的赋值是无法被Vue探测到的,所以其后添加的属性无法变为响应式。这样子似乎一切都说的通,但是关键是这种变量提升这个东西我之前从未在Vue中遇到过,我查阅资料也发现根本不存在这种机制。于是这个说法被PASS。

不过这个过程中我也复习了一下生命周期函数和Vue中数组对象的处理:

Vue生命周期

引用官方的一张图:

值得注意的是:data,methods等都是在beforeCreated与created之间进行的。经常使用的mounted已经是在模板已经编译完成之后了。

Vue中对数组和对象的处理

对于动态更新DOM最基本的原理是定义一个变量的getter和setter,在其中触发更新DOM操作,对于数组,我们整个赋值,我们可以使用getter和setter监听,但是如果我们操作数组内部的元素,比如arr[1] = 0,这样的操作我们是无法监听到的,因此在Vue中,如果这样给data赋值,这个值并不会变为响应式,而是一个普通的值。具体有

  1. 数组
    1. 通过索引去修改数组中的值,arr[i] = val
    2. 直接修改数组的长度,arr.length = n
  2. 对象
    1. 给对象添加属性(包括data根节点,比如没有在data中初始化某个值,在后续直接使用this.data1 = {},这样data1无法变为响应式,dev模式下console会报错)
    2. 删除对象属性,(包括data根节点)

对于以上的问题,Vue提供了vm.$set这个api来解决。

对于数组的第一个问题,可以使用以下方式来解决:

vm.$set(vm.data1,index,val)

对于数组的第二个问题,可以使用数组的方法来解决:

vm.data1.splice(3) //data1会被切割为0-3项

原因是Vue在内部也修改了Array原型链上的方法,使其能够被Vue监听到,包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

思路二:请求异步的问题

这一次我似乎意识到了可能是异步的问题,因为之前玩canvas时,也是一直结果达不到预期,结果是img标签的onload事件是一个异步事件,导致结果不对。再仔细看这次的代码,可以发现,在第一次请求回调内部,由于接下来的子请求又是异步事件,所以直接执行了最后的this.data1 = data1。在子请求回掉函数执行时,data1已经被挂载到vm.data中,此时再对其进行添加属性处理,相当于是上面的对象添加属性的问题。所以必然是非响应式的。于是我this.data1 = data1放到子请求的回调中,每接受一次回调执行一次,虽然感觉这样很消耗性能。

但是结果是,data1数组中只有一个值的属性变为了响应式,其他的数组值仍然是非响应的。。。至少结果逼近真相了。

思路三:Vue对于data赋值的性能优化

在Vue中,有一个异步更新队列,在Vue检测到data的值改变时,会将其写入异步更新队列中,在下一个事件循环中去进行DOM跟新等等。并且在这个队列中,会对数据写入进行优化,比如this.data1原来等于1,接下来执行了两句赋值,this.data1 = 3;this.data1 = 1;并且这两句在同一个tick(事件循环队列)中,那么Vue就不会对data1进行操作了。

那么这里极可能是在极短的时间内对data1进行了两次赋值,Vue将这两次操作优化,第一次是整个赋值,第二个则只对修改的部分进行了变更,但是前面又说道,Vue没办法探测数据内容或对象属性的添加操作。所以第一个是响应式的,而第二个则不是响应式的。

所以唯一的解决办法就是只进行一次data1的根赋值,所以我想到了 axios.all这个api,它是在所有请求完成之后返回并进行回调函数。在该回调函数中进行数据处理,再直接将data1赋给vm实例上。

成功解决

总结

还是不太够仔细,异步这样的问题不能立即看出来,导致前面走了很大的弯,还好后面分析过程比较正确,找到了问题所在。不过也好,重温了一下,生命周期,Vue对数组和对象的处理等等。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :