这是我参与更文挑战的第10天,活动详情查看: 更文挑战
一,前言
上篇,主要介绍了对象数据变化的观测情况,涉及以下几个点:
- 实现了对象老属性值变更为对象、数组时的深层观测处理;
- 结合实现原理,说明了对象新增属性不能被观测到的原因,以及如何实现对象新增属性的数据观测;
本篇,数组数据变化的观测情况(包含数组中新增对象、数组、普通值的情况)
二,在数组中,新增对象、数组、普通值的观测问题
1,问题分析
截止至当前版本,针对数组类型进行了以下处理:
- 重写了数组链上的方法,实现了对引起原数组变化的7个原型方法的劫持;
- 对数组中每一项调用
observe进行递归处理,实现数组类型的深层观测;
测试:向数组arr中新增对象、数组、普通值时,是否会触发数据更新?
let vm = new Vue({
el: '#app',
data() {
return { arr: [{ name: "Brave" }, 100] }
}
});
vm.arr.push({a:100});
vm.arr[2].a = 200;
注意:由于observe仅处理对象类型,所以数组中的普通值不会被观测;
展开说明:
data根数据通过前期处理一定是一个对象,数据递归观测的入口是observe(data)方法,当内部存在数组类型的数据时,会调用observeArray进行处理,observeArray对继续对数组中的每一项调用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)
// 劫持到数组变化后,尚未实现处理逻辑
}
});
目前,虽然前面通过重写数组的7个原型方法,已经实现了对数组类型的数据劫持(当向数组中添加数据时会触发数据劫持),但具体的处理逻辑尚未实现,目前只会打印出日志;
在官方版本 Vue2 中,向数组中新增对象、修改新增对象的属性,都可以触发数据更新;
2,思路分析
以push方法为例,分析原型方法push的实现逻辑:
- 首先,需要调用
push原生方法逻辑,实现原数组的更新操作; - 然后,如果
push参数中存在对象类型,需要继续进行劫持,实现对数组中新增对象的数据观测;
注意:
push、pop、shift、unshift、reverse、sort、splice这7个方法的入参数量是不一致的,例如push方法就支持传入多个参数vm.arr.push({a:100},{b:200},{c:300}),所以入参为...args;
3,代码实现
当调用arr.push方法时,传入的参数为对象类型时,需要再次进行观测
// src/observe/array.js
methods.forEach(method => {
arrayMethods[method] = function (...args) {
console.log('数组的方法进行重写操作 method = ' + method)
// ****** 调用`push`原生方法逻辑,实现原数组的更新操作 ******
// AOP:before 原生方法扩展...
oldArrayPrototype[method].call(this, ...args) // 绑定到当前调用上下文
// AOP::after 原生方法扩展...
// ****** 处理数组的新增数据 ******
// 只有splice、push、unshift能新增数据,需要实现数据响应式;
let inserted = null; // 收集新增的数据
switch (method) {
// splice 方法做增加,一定会有第三个参数:arr.splice(0,0,100)
case 'splice': // splice:修改、删除、添加
// 获取新增数据:从第三个参数开始都是新增数据
inserted = args.slice(2);
break;
case 'push': // push:向前增加
// 不写 break 会穿透到 unshift 处理
case 'unshift': // unshift:向后增加
// push、unshift 方法的参数全部为新增数据
inserted = args
break;
}
// 遍历 inserted 数组中的新增数据,对象类型需要继续进行观测
}
});
- 通过
oldArrayPrototype[method].call(this, ...args)执行push原生方法逻辑并绑定当前上下文,实现原数组的更新操作; - 收集通过
splice、push、unshift方法新增的数据,放入inserted数组; - 遍历
inserted数组,当数据为对象类型时,需要继续进行观测;
4,问题 1:如何实现新增对象的深层观测
问题分析
Observer类中的原型方法observeArray实现了数组的深层劫持,但此方法并未对外导出;
所以,在当前模块中遍历inserted数组时,就无法调用到Observer类中的observeArray方法实现数据观测;
解决方案
思路:让当前数组或对象与
Observer实例产生一个关联关系;
在Observer初始化时,为当前数组或对象value添加自定义属性__ob__,使value和Observer实例之间产生关联:value.__ob__ = this
value:为当前数组或对象,添加自定义属性__ob__ = this;(在 observe 方法中,只有值为对象类型时即数组或对象,才会执行new Observer创建实例,因此value必为数组或对象)this:为当前Observer实例,通过实例可以调用到observeArray方法;
这样,就可以在src/observe/array.js模块遍历inserted数组时,调用到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__属性获取到当前Observer实例,进而通过Observer实例调用到observeArray方法,实现数组的深层观测;
// 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方法,从而使新增的对象或数组成为响应式数据;
5,问题 2:死循环问题
测试死循环问题
当前版本代码运行时会出现死循环:
思考:在Observer类中,只添加了value.__ob__ = this这一句代码,为什么就导致了死循环?
// 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可能是对象也可能是数组,会先为value添加__ob__属性,值为当前Observer实例; - 继续向下,当
value为对象类型时会执行walk方法:遍历对象中的所有属性并依次执行defineReactive方法; - 当
walk方法遍历属性时,对象中新添加的__ob__属性也会被遍历出来,__ob__属性的值为当前Observer实例,也是一个对象,所以,在defineReactive方法中会继续通过observe进行递归处理,会继续执行new Observer继续添加__ob__属性,值为新的Observer实例....,这样就造成了死循环;
得出结论:在walk中,只要__ob__属性被遍历出来,后续通过Object.defineProperty处理后,就造成死循环;
死循环问题的解决方案
将属性设置为不可枚举
为对象或属性添加__ob__属性时,可以通过Object.defineProperty将__ob__属性配置为不可被枚举;
这样,在walk方法遍历对象属性时,__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);
}
}
}
重新执行测试,死循环问题解决:
冻结属性方案:
__ob__属性被冻结后,仅仅只是不能被修改了,依然能够被遍历出来,无法解决死循环问题;
对象被重复观测的问题
在Observer构造方法中,会为对象添加__ob__属性,之后:
- 数组会执行
observeArray方法:对每一项执行observe方法 - 对象会执行
walk方法:对每一个属性执行defineReactive方法,在defineReactive方法中,如果是对象还会继续递归执行observe方法;
当执行observe方法时,如果值为对象,将会继续创建Observer实例进行递归观测;
// todo 添加一个对象被重复观测的例子
结论:当对象被添加__ob__属性标识后,代表着当前对象已经被创建过Observer实例了,即当前对象已经被深层观测过了,在之后的处理中应避免重复观测;
// src/observe/index.js
export function observe(value) {
if (!isObject(value)) {
return;
}
if(value.__ob__){
console.log("当前数据已经被观测过了,value = "+ value)
return;
}
return new Observer(value);
}
所以,
Observer使用类实现而不用函数还有其他好处 首先,可以通过Observer实例上是否有__ob__数据判断对象是否已经被观测过了,从而避免对象被重复进行观测; 其次,如果通过函数实现,__ob__就要添加到链上:value.__proto__.__ob__,这样一来所有的对象就都有__ob__属性了...这不是乱套了么...
6,功能测试
let vm = new Vue({
el: '#app',
data() {
return { arr: [{ name: "Brave"}, {} ] }
}
});
// 测试:数组的深层观测问题
// 不会更新,当前仅重写了数组链上的方法,数组中的数组没有被递归处理
// vm.arr[0].push("123")
// 测试:修改新对象的属性值
// 不会更新,当前新对象没有被被观测
// vm.message = { a: 100 }
// vm.message.a = 200;
// 测试:修改数组中已存在的属性值
// 会更新,数组中的对象会被递归观测
// vm.arr[0].name = "BraveWang";
// 测试:修改数组的索引
// 不会更新,Vue2 没有对数组索引进行劫持
// vm.arr[1] = 200;
// 实现数组中新对象的深层观测-push逻辑的重写
// vm.arr.push({a:100});
// vm.arr[2].a = 200;
// 测试:数组新增对象的深层观测
vm.arr.push({ a: 100 }, { b: 200 }, { c: 300 });
console.log(vm.arr)
执行结果:
向数组中新增对象,数组中的老对象和新对象都会添加__ob__属性,并且对象中的属性都会被劫持;
三,结尾
本篇,主要介绍了数组数据变化的观测情况:
- 实现了数组数据变化被劫持后,重写原型方法的具体逻辑;
- 数组各种数据变化时的观测情况分析;
至此,数据劫持就全部完成了
下一篇,数据渲染的流程
维护日志
- 20210111:重新梳理文章内容,优化部分内容语义,添加描述内容中的代码显示高亮;
- 20210112:补充大量文字说明,重新调整目录结构,添加对象重复观测处理;