为什么Vue不能检测数组和对象的变化?

3,372 阅读4分钟

前言

在Vue2的官方文档中,不知道各位小伙伴注意到文档中有明确的表示:

image.png

检测变化的注意事项

检测变化的注意事项

关于对象的问题

  1. 新增未声明的属性
  2. 删除属性
var vm = new Vue({  
    data:{  
        message: 'hello'
    }  
})

vm.message = 'hello vue' // `vm.message` 是响应式的  

vm.describe = 'hello Vue.js'  // `vm.describe` 是非响应式的

// 非响应式的
delete vm.message

源码调试如下:

image.png

image.png

image.png

通过调试我们可以看到,不存在data对象的属性、删除属性,都无法转化成响应式

Vue为什么不能检测对象的变动

  1. Vue 无法检测实例被创建时不存在于 data 中的 property

由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

  1. Vue 不允许动态添加响应式 property,所以你必须在初始化实例前声明所有响应式 property

getter/setter只能追踪一个数据是否被更改,无法追踪新增属性和删除属性

关于数组的问题

  1. 当你利用索引直接设置一个数组项时
  2. 当你修改数组的长度时
var vm = new Vue({  
    data: {  
        items: ['a', 'b', 'c']  
    }  
})

vm.items[1] = 'x' // 不是响应性的  
vm.items.length = 2 // 不是响应性的

视图没有发生变化

Vue为什么不能检测数组的变动

首先,先说结论:为什么vue没有提供对数组属性的监听, 原因是:性能问题。

即:性能代价和获得的用户体验收益不成正比。

变化侦测

什么是变化侦测?

变化侦测就是追踪状态,亦或者说是数据的变化,一旦发生了变化,就要去更新视图。

变化侦测代码

code.juejin.cn/pen/7163530…

源码调试

vue github地址

注: 本次调试使用的是v2.7.14版本。

Observer源码

image.png

我们发现,当侦测的数据是数组时,如果是数组,则判断是否支持__proto__,如果支持则使用protoAugment去覆盖原型

很明显,源码中对数组做了单独的处理.

为什么会停止对数据的检测呢?

我们修改一段源码去一探究竟

修改源码

if(Array.isArray(value)){
    value.forEach((item, index) => {
      defineReactive(value, index, item, undefined, shallow, mock);
    });
    
    // 移除以前的代码
    ...
}

image.png

验证

我们再次调试后,让我们见证奇迹吧

image.png

点击按钮后,视图也会发生了变化

image.png

如此简单的事情,尤大为何多此一举呢?

深入探究

我们用循环将数组数据塞入1000个元素,数据更新为:[0, 1, 2..., 998, 999]。

然后在set中加入时间也是ms级

image.png

更可况,console.log输出也是及占时间的

肯定在其他的问题上

我们再次将数组的每一项换成一个Objcet,让其更接近项目

let temps = []
for (let i = 0; i < 1000; i++) {
    temps.push({
        index: i,
        value: i + '',
        arr: [i]
    })
}
this.items = temps


this.items[0] = {
    index: 0,
    value: 999 + '',
    arr: [999]
}

再次运行后,页面更新也是ms级,没有一丝的卡顿

此时,设置数组的某一项值的研究暂时没有眉目了。

寻找修改数组的长度的突破

于是,我们将数组的长度修改为10

 setLen() {
    this.items.length = 10
}

视图并没有进行更新, 原因也是isArray()的代码中需要知道newArray

image.png

此时,我们要是知道新的数据了,我们都可以直接去更新了

Array的问题

没有更新的原因,就是vue是在拦截器中获取vue实例

而length只是一个属性

正是这种处理方式,上述的操作是拦截不到的

源码如下: image.png

图解如下: image.png

我们将源码还原后,再次改造

image.png

发现也是无法响应的

Observer中的isArray已还原

查阅

源码研究至此,暂时没有什么新的突破,于是查阅了一下资料,果然有新发现

有兴趣的掘友们,可以继续研究,我们一起探讨

最后期待您的一键三连

为什么vue没有提供对数组属性的监听

image.png

Proxy

Vue3是基于 JavaScript Proxy(代理) 实现响应式的。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

但Proxy 对 IE 不友好,vue3 在检测到使用 IE 的情况下(包括 IE11),会自动降级为 Object.defineProperty的数据监听系统

API

当然Vue中也给出了详细的解决方案:

  1. Vue.set( target, propertyName/index, valu…
  2. Vue.delete( target, propertyName/index )
  3. Array.prototype.splice()

总结

  1. 由于JavaScript原生不够强大,导致了框架设计的局限性。(ES5的Object.defineProperty),导致Vue无法追踪到数据的变化
  2. 性能与用户体验,有时需要进行取舍

参考资料

  1. vue2.js
  2. vue3.js
  3. 记一次思否问答的问题思考:Vue为什么不能检测数组变动
  4. 为什么vue没有提供对数组属性的监听