关于vue数组不能赋值的深入思考

前提:
因为想对数组进行一些新增的操作,在属性上直接赋值,预示有了这样的代码 vm.items[indexOfItem] = newValue,但页面上并没有展示我想要的数据。

探索历程:
1、’在vue官方文档中,发现
官方的原文:由于 JavaScript 的限制, Vue 不能检测以下变动的数组:
当你利用索引直接设置一个项时,例如:

vm.items[indexOfItem] = newValue

当你修改数组的长度时,例如:

 vm.items.length = newLength

于是临时改成了

this.getAllOperator.splice(index, 0, res.body.data);

2、因为项目急,当时并没有深入研究,最近闲出来仔细研究一下
于是,网上搜了一下vue为什么不能检测数组变动。
vue监测数据变动是通过Object.defineProperty实现的,
在这里插入图片描述
但在构造函数中已经为所有属性做了这个检测绑定操作
意思就是Object.defineProperty是可以通过索引属性来设置属性的访问器属性的。
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。数组的索引也是属性,所以我们是可以监听到数组元素的变化的。

var arr = [1,2,3,4]
arr.forEach((item,index)=>{
    Object.defineProperty(arr,index,{
        set:function(val){
            console.log('set')
            item = val
        },
        get:function(val){
            console.log('get')
            return item
        }
    })
})
arr[1]; // get  2
arr[1] = 1; // set  1

但是对数组进行新增,删除等操作时,就不会触发监听事件。

于是搜到了一个大大写的文章
Vue为什么不能检测数组变动
大大在文中提到 数组是可以被监听的,那为什么vue不能检测vm.items[indexOfItem] = newValue导致的数组元素改变呢,哪怕这个下标所对应的元素是存在的,且被监听了的?
通过vue的源码测试,对数据进行监听(以下代码来自于上述链接的文章里)

var Observer = function Observer(value) {
 this.value = value;
 this.dep = new Dep();
 def(value, '_ob_', this);
 if (Arry.isArray(value)) {
 	var augment = hasProto? protoAugment : copyAugment;
 	augment(value, arrayMethods, arrayKeys);
 	this.observeArray(value);
 } else {
 	this.walk(value);
 }
};
Observer.prototype.walk = function walk(obj) {
	var keys = Object.keys(obj);
	for (var i = 0; i < keys.length; i++) {
		defineReactive(obj, keys[i]);
	}
};

通过代码可以看到,当数据是数组是,会停止对数据属性的检测。

var Observer = function Observer(value) {
 this.value = value;
 this.dep = new Dep();
 def(value, '_ob_', this);
 if (Arry.isArray(value)) {
 	// var augment = hasProto? protoAugment : copyAugment;
 	// augment(value, arrayMethods, arrayKeys);
 	// this.observeArray(value);
 	this.walkArr(value);
 } else {
 	this.walk(value);
 }
};
Observer.prototype.walk = function walk(obj) {
	var keys = Object.keys(obj);
	for (var i = 0; i < keys.length; i++) {
		defineReactive(obj, keys[i]);
	}
};
Observer.prototype.walkArr = function walkArr(arr) {
	for (var i = 0; i < arr.length; i++) {
		defineReactive(arr, arr[i], i);
	}
};

在这里插入图片描述
文中,大大在defineReactive函数中的get,set打印数据,方便知道调用了get和set,图片中加了简单的判断,只看数组元素的get和set。
测试vm.items[indexOfItem] = newValue改变数组元素能不能被监测到,并响应式的渲染页面
在上述链接中有完整的测试过程。
结论是 vue中是可以实现响应式更新的,也就是 vm.items[indexOfItem] = newValue改变数组元素能被监测到,并响应式的渲染页面。
上述链接中,大大请教了尤大为什么vue没有加入这一功能,
在这里插入图片描述
所以,事实上, Object.defineProperty 本身是可以监控到数组下标的变化的,只是在 Vue 的实现中,从性能/体验的性价比考虑,放弃了这个特性。

3、vue3.0中,响应式数据部分放弃了Object.defineProperty ,使用 Proxy 来代替它。
于是乎,我又搜到了一篇文章(大数据的时代真的太方便了)
为什么Vue3.0使用Proxy实现数据监听(defineProperty表示不背这个锅)
在这个链接中,大大通过以下三个问题分析为什么vue选择弃用Object.defineProperty。
1、Object.defineProperty 真的无法监测数组下标的变化吗?
2、分析vue2.x中对数组 Observe 部分源码
3、对比 Object.defineProperty 和 Proxy

关于第一个问题,我们之前已经得出结论:
事实上, Object.defineProperty 本身是可以监控到数组下标的变化的,只是在 Vue 的实现中,从性能/体验的性价比考虑,放弃了这个特性。

关于第二个问题,我们之前也能得出vue的Observer对数组做了单独的处理,在第二个链接中,大大更为详细的介绍了vue对数组的observer做了哪些处理。

关于第三个问题,在第二个链接中,作者大大做了以下总结
1、Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。
由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,不需要遍历操作。
2、Object.defineProperty对新增属性需要手动进行Observe。
由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。也正是因为这个原因,使用vue给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。
如果采用 proxy 实现, Proxy 通过 set(target, propKey, value, receiver) 拦截对象属性的设置,是可以拦截到对象的新增属性的。不止如此, Proxy 对数组的方法也可以监测到。

在链接中,作者大大提到 Proxy支持13种拦截操作,这是defineProperty所不具有的
get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.foo 和 proxy[‘foo’] 。
set(target, propKey, value, receiver):拦截对象属性的设置,比如 proxy.foo = v 或 proxy[‘foo’] = v ,返回一个布尔值。
has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。
deleteProperty(target, propKey):拦截 delete proxy[propKey] 的操作,返回一个布尔值。
ownKeys(target):拦截 Object.getOwnPropertyNames(proxy) 、 Object.getOwnPropertySymbols(proxy) 、 Object.keys(proxy) 、 for…in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target, propKey):拦截 Object.getOwnPropertyDescriptor(proxy, propKey) ,返回属性的描述对象。
defineProperty(target, propKey, propDesc):拦截 Object.defineProperty(proxy, propKey, propDesc) 、 Object.defineProperties(proxy, propDescs) ,返回一个布尔值。
preventExtensions(target):拦截 Object.preventExtensions(proxy) ,返回一个布尔值。
getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy) ,返回一个对象。
isExtensible(target):拦截 Object.isExtensible(proxy) ,返回一个布尔值。
setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto) ,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如 proxy(…args) 、 proxy.call(object, …args) 、 proxy.apply(…) 。
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(…args) 。

由于我的vue版本是2.6.12,所以并未进行测试,后续再跟上。

我也只是个站在巨人肩膀上的小菜鸡,感谢前面两位作者提供的参考文章。

Logo

前往低代码交流专区

更多推荐