数组的单层劫持
1. 为什么原本可以实现对数组索引的观测,Vue 却选择了不支持呢?
Vue2.x 中,不支持通过修改数组索引和长度的数据劫持
主要是考虑了性能问题,比如,数组中的数据量非常大时:
let vm = new Vue({
el: '#app',
data() {
return { arr:new Array(9999) }
}
});
这时,为了实现数组索引劫持,需要对数组中每一项进行处理,实现了对数组的深层观测;
所以,权衡性能和需求,Vue 没有对数组类型的数组使用 Object.defineProperty 进行递归劫持,而是通过对能够导致原数组变化的 7 个方法进行拦截和重写实现了数据劫持;
当然,这也就导致了在 Vue 中无法通过直接修改索引、length 触发视图的更新
2,数组的劫持思路
核心目标是要实现数组的响应式:
Vue 认为这 7 个方法能够改变原数组:push、pop、splice、shift、unshift、reverse、sort
所以,只要对这 7 个方法进行处理,就能劫持到数组的数据变化,实现数组数据的响应式
对象属性深层劫持的实现:
-
数据观测observe 方法,
-
如果数据为对象类型就 new Observer
-
Observer 初始化时,会遍历对象属性,逐一递归 Object.defineProperty
数组也是对象,所以,要把数组的处理逻辑单独拆出来。即对 7 个变异方法进行重写
// 判断是否是数组
export function isArray(val) {
return Array.isArray(val)
}
import { arrayMethods } from "./array";
class Observer {
constructor(value) {
if(isArray(value)){
// 对数组类型进行单独处理:重写 7 个变异方法
} else {
this.walk(value);
}
}
}
3,数组方法的拦截思路
-
重写方法需要在原生方法基础上,实现对数据变化的劫持操作
-
仅对响应式数据中的数组进行方法重写,不能影响非响应式数组
所以,对响应式数据中数组这 7 个方法进行拦截,即优先使用重写方法,其他方法还走原生逻辑
数组方法的查找,先查找自己身上的方法(即重写方法),找不到再去链上查(原生方法)
4,数组方法重写的实现
// 拿到数组的原型方法let
oldArrayPrototype = Array.prototype;// 原型继承,将原型链向后移动
arrayMethods.__proto__ == oldArrayPrototypeexport
let arrayMethods = Object.create(oldArrayPrototype);
// 重写能够导致原数组变化的七个方法let
methods = [ 'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']
// 在数组自身上进行方法重写,对链上的同名方法进行拦截
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 有值就是数组
}
});
添加 new Observer 时,对数组方法重写的逻辑:
import { arrayMethods } from "./array";
class Observer {
constructor(value) { // 分别处理 value 为数组和对象两种情况
if(isArray(value)){
value.__proto__ = arrayMethods; // 更改数组的原型方法
} else {
this.walk(value);
}
}
}
测试数组方法的重写:
数组的链:
- array.proto:包含 7 个重写方法
- array.proto.proto:原始方法
5,数组方法拦截的实现
function initData(vm) {
let data = vm.$options.data;
data = isFunction(data) ? data.call(vm) : data;
observe(data); // 在observe方法中new Observer执行后,数组的原型方法已完成重写
// 测试数组方法的拦截效果
data.arr.push(666);
data.arr.pop()
}
- arrayMethods.push:会在数组自身找到重写的 push 方法,不会继续到链上查找,实现拦截
- arrayMethods.pop:数组自身没找到重写方法,继续到链上找到原生 pop 方法
数组的深层劫持
1,代码实现
通过以上分析,实现数组的深层劫持,需要处理两种情况:
- 数组中嵌套数组
- 数组中嵌套对象
class Observer {
constructor(value) {
if (isArray(value)) {
value.__proto__ = arrayMethods;
this.observeArray(value); // 对数组数据类型进行深层观测
} else {
this.walk(value);
}
}
/**
* 遍历数组,对数组中的对象进行递归观测
* 1)[[]] 数组套数组
* 2)[{}] 数组套对象
*/
observeArray(data) {
// observe方法内,如果是对象类型,继续 new Observer 进行递归处理
data.forEach(item => observe(item))
}
}
observeArray 方法:为数组中的每一项,调用 observe 进行深层观测处理
这样,数组中的数组 [ [] ]、数组中的对象 [ {} ] ,两种情况都实现了数据的深层观测
2,问题分析
由于仅重写了数组的部分原型方法,未对每一项进行数据劫持
所以,数组中的普通值,不能被观测;(仅重写数组方法,递归中对值类型不再处理)
数组中的引用类型,能够被观测;(observe 实现对象类型深层观测)
举例分析:
- 若 vm.arr[0] 为普通值:(仅重写数组方法,递归中对值类型不再处理)
vm.arr[0] = 100 操作数组索引,不会触发视图更新(没对数组索引观测)
- 若 vm.arr[0] 为对象:(observe 实现对象类型深层观测)
vm.arr[0].a = 1 修改对象属性,会触发视图更新