一,前言
上篇,主要介绍了对象数据变化的观测情况,涉及以下几个点:
实现了对象老属性值变更为对象、数组时的深层观测处理;
结合实现原理,说明了对象新增属性不能被观测的原因,及如何实现数据观测;
本篇,数组数据变化的观测情况(数组中,新增对象、数组、普通值的情况)
二,数组中,新增对象、数组、普通值的观测问题
1,问题分析
向数组 arr 中新增对象、数组、普通值,会触发数据更新吗?
let vm = new Vue({
el: '#app',
data() {
return { arr: [{ name: "Brave" }, 100] }
}
});
vm.arr.push({a:100});
vm.arr[2].a = 200;
截止至当前版本,针对数组类型的处理:
-
重写了数组链上的方法,能够对引起原数组变化的 7 个原型方法进行劫持;
-
对数组中的每一项递归调用 observe 进行处理,使数组类型实现递归观测;
由于 observe 仅处理对象类型,所以数组中的普通值不会被观测;
虽然已经实现了数组的数据劫持,但尚未实现数据劫持后的具体逻辑:
// src/Observer/array.js
let oldArrayPrototype = Array.prototype;
export let arrayMethods = Object.create(oldArrayPrototype);
let methods = [
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice'
]
methods.forEach(method => {
arrayMethods[method] = function () {
console.log('数组的方法进行重写操作 method = ' + method)
// 劫持到数组变化后,尚未实现处理逻辑
}
});
所以,向数组中添加内容,是能够触发数据劫持的,但还没有实现劫持后的具体逻辑
在 Vue2.x 中,向数组中新增对象,及修改新增对象的属性,都是可以触发更新的;
2,思路分析
重写 push 方法逻辑:
由于 7 个方法的入参数量不一致,例如 push 可以传入多个参数
3,代码实现
当 push 的参数为对象类型时,需要再次进行观测
// src/observe/array.js
methods.forEach(method => {
// 当前的外部调用:arr.push
arrayMethods[method] = function (...args) {
console.log('数组的方法进行重写操作 method = ' + method)
// AOP:before 原生方法扩展...
// 调用数组原生方法逻辑(绑定到当前调用上下文)
oldArrayPrototype[method].call(this, ...args)
// AOP::after 原生方法扩展...
// 数组新增的属性如果是属性,要继续观测
// 哪些方法有增加数组的功能: splice push unshift
let inserted = [];
switch (method) {
// arr.splice(0,0,100) 如果splice方法用于增加,一定有第三个参数,从第三个开始都是添加的内容
case 'splice': // 修改 删除 添加
inserted = args.slice(2); // splice方法从第三个参数起是新增数据
case 'push': // 向前增加
case 'unshift': // 向后增加
inserted = args // push、unshift的参数就是新增
break;
}
// 遍历inserted数组,看一下它是否需要进行劫持
}
});
当 push 的参数为对象类型时,需继续对其进行观测;
问题 1
数组深层劫持的 observeArray 方法,在 Observer 类中
由于没有导出,在 src/observe/array.js 的 methods.forEach 中是访问不到的
Observer 类中也拿不到 vm,
所以为当前 this 添加自定义属性进行关联:value.ob = this;
value:为数组或对象添加自定义属性__ob__ = this,
this:为当前 Observer 类的实例,实例上就有 observeArray 方法;
如此,便可在src/observe/array.js 的 methods.forEach 中,调用到 observeArray 方法实现数组的深层劫持;
// src/observe/index.js
class Observer {
constructor(value) {
// value:为数组或对象添加自定义属性__ob__ = this,
// this:为当前 Observer 类的实例,实例上就有 observeArray 方法;
value.__ob__ = this;
if (isArray(value)) {
value.__proto__ = arrayMethods;
this.observeArray(value);
} else {
this.walk(value);
}
}
}
添加了__ob__后的数组,调用了 push 方法,所以能够通过__ob__属性获取到 ob
// src/observe/array.js
methods.forEach(method => {
arrayMethods[method] = function (...args) {
oldArrayPrototype[method].call(this, ...args)
let inserted = null;
let ob = this.__ob__; // 通过 __ob__ 属性获取到 ob
switch (method) {
case 'splice':
inserted = args.slice(2);
case 'push':
case 'unshift':
inserted = args
break;
}
// observeArray:内部遍历inserted数组,调用observe方法,是对象就new Observer,继续深层观测
if(inserted)ob.observeArray(inserted);// inserted 有值就是数组
}
});
所以,当向数组 push 对象或数组时,会继续走 observeArray 方法,使对象或数组成为响应式
问题 2
运行会导致死循环
// src/observe/index.js
class Observer {
constructor(value) {
value.__ob__ = this;
if (isArray(value)) {
value.__proto__ = arrayMethods;
this.observeArray(value);
} else {
this.walk(value);
}
}
walk(data) {
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
});
}
}
在 Observer 类中,由于 value.ob = this; 这段代码
value 如果是对象,会走到 this.walk(value); 方法,继续循环对象的属性,
这时,属性__ob__会被循环出来,而__ob__又是一个对象,且在这个对象上还有__ob__
所以,在 walk 循环中对属性__ob__做 defineProperty 后,它的值还是一个对象,就无限递归造成了死循环
value 是对象就会进入 walk 方法,循环 value 对象中的所有属性,
其中__ob__属性将被循环出来,而 ob 就是当前实例,实际也是一个对象,会被继续观测,造成死循环
所以,这段代码不能这么写,即__ob__不能被遍历,否则遍历出来后就会被defineProperty,造成死循环;
冻结:属性冻结后只是不能被修改了,但还是能被遍历出来的
需要使用 defineProperty 定义__ob__ 属性,并将 ob 属性配置为不可被枚举
// src/observe/index.js
class Observer {
constructor(value) {
// value.__ob__ = this; // 可被遍历枚举,会造成死循环
// 定义__ob__ 属性为不可被枚举,防止对象在进入walk都继续defineProperty,造成死循环
Object.defineProperty(value, '__ob__', {
value:this,
enumerable:false // 不可被枚举
});
if (isArray(value)) {
value.__proto__ = arrayMethods;
this.observeArray(value);
} else {
this.walk(value);
}
}
}
再执行,问题解决:
三,结尾
本篇,主要介绍了数组数据变化的观测情况:
- 实现了数组数据变化被劫持后,已重写原型方法的具体逻辑;
- 数组各种数据变化时的观测情况分析;
至此,数据劫持就全部完成了
下一篇,数据渲染的流程