【Vue2.x 源码学习】第五篇 - 数组的劫持
一,前言
上篇,主要介绍了 Vue 数据初始化流程中,对象属性的深层劫持是如何实现的
核心思路就是递归,主要流程如下;
本篇,继续介绍 Vue 数据初始化流程中,对于数组类型的劫持
二,对象劫持回顾
1,Demo
data 数据中对象属性的深层观测,即对象属性为对象(包含多层)的情况
let vm = new Vue({
el: '#app',
data() {
return { message: 'Hello Vue', obj: { key: "val" }, a: { a: { a: {} } } }
});

data 中的属性为数组时,那么,当Vue 是如何进行处理的?
三,数组类型的处理
1,当前逻辑分析
按照当前版本的处理逻辑,所有对象类型会对被进行深层观测,数组也不例外
let vm = new Vue({
el: '#app',
data() {
return { message: 'Hello Vue', obj: { key: "val" }, arr:[1,2,3]}
}
});

可以看到,数组中的每一项,都被添加了 get、set 方法,也就相当于实现了对数组的深层观测
备注:Object.defineProperty支持数组数据类型的劫持
2,Vue 对性能的权衡
在 Vue2.x 中,不支持通过修改数组索引和长度的数据劫持;
那么,为什么原本可以实现对数组索引的观测,Vue 却选择了不支持呢?
主要是考虑了性能问题,比如,数组中的数据量非常大时:
let vm = new Vue({
el: '#app',
data() {
return { arr:new Array(9999) }
}
});
这时,数组中 9999 条数据,将全部被添加 get、set 方法
而这一套操作就比较费劲了:为了实现数组索引劫持,需要对数组中每一项进行处理
还有就是,虽然数组能够通过 defineProperty 实现对索引更新劫持
但在实际开发场景真的需要吗?似乎很少会使用 ``` arr[888] = x ``` 这种操作
所以,权衡性能和需求,Vue 源码中没有采用 defineProperty 对数组进行处理
当然,这也就导致了在 Vue 中无法通过直接修改索引、length 触发视图的更新
3,数组的劫持思路
核心目标是要实现数组的响应式:
Vue 认为这 7 个方法能够改变原数组:push、pop、splice、shift、unshift、reverse、sort
所以,只要对这 7 个方法进行处理,就能劫持到数组的数据变化,实现数组数据的响应式
备注:这种实现思路,也直接导致了 vue2 修改数组的索引和长度不能触发视图更新
梳理对象属性深层劫持的实现:
数组也是对象,所以,要把数组的处理逻辑单独拆出来。即对 7 个变异方法进行重写
// src/utils
/**
* 判断是否是数组
* @param {*} val
* @returns
*/
export function isArray(val) {
return Array.isArray(val)
}
// src/observe/index.js
import { arrayMethods } from "./array";
class Observer {
constructor(value) {
if(isArray(value)){
// 对数组类型进行单独处理:重写 7 个变异方法
}else{
this.walk(value);
}
}
}
4,数组方法的拦截思路
重写方法需要在原生方法基础上,实现对数据变化的劫持操作
仅对响应式数据中的数组进行方法重写,不能影响非响应式数组
所以,对响应式数据中数组这 7 个方法进行拦截,即优先使用重写方法,其他方法还走原生逻辑
数组方法的查找,先查找自己身上的方法(即重写方法),找不到再去链上查(原生方法)
5,数组方法重写的实现
// src/Observer/array.js
// 拿到数组的原型方法
let oldArrayPrototype = Array.prototype;
// 原型继承,将原型链向后移动 arrayMethods.__proto__ == oldArrayPrototype
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)
}
});
添加 new Observer 时,对数组方法重写的逻辑:
// src/observe/index.js
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__:原始方法
6,数组方法拦截的实现
// src/state.js#initData
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方法
四,结尾
又成功水了一篇,还剩 16 篇
本篇主要介绍了 Vue 数据初始化流程中,数组类型的数据劫持,核心有以下几点:
出于对性能的考虑,Vue 没有对数组类型的数组使用 Object.defineProperty 进行递归劫持,而是通过对能够导致原数组变化的 7 个方法进行拦截和重写实现了数据劫持
下一篇,实例取值代理的实现