这是我参与更文挑战的第5天,活动详情查看: 更文挑战
一,前言
上篇,主要介绍了 Vue 数据初始化流程中,对象属性的深层劫持是如何实现的
核心思路就是递归,主要流程如下;
- 1.通过 data = isFunction(data) ? data.call(vm) : data;处理后的 data 一定是对象类型
- 2.通过 data = observe(data)处理后的 data 就实现了数据的响应式(目前只有劫持)
- 3.observe 方法最终返回一个 Observer 类
- 4.Observer 类初始化时,通过 walk 遍历属性
- 5.对每一个属性进行 defineReactive(Object.defineProperty)就实现对象属性的单层数据劫持
- 6.在 defineReactive 中,如果属性值为对象类型就继续调用 observe 对当前的对象属性进行观测(即递归步骤 3~5),这样就实现了对象属性的深层数据劫持
本篇,继续介绍 Vue 数据初始化流程中,对于数组类型的劫持;
二,提出问题:对象劫持实现了,数组类型如何处理?
当前代码已经支持,当 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 方法,相当于实现了数组的深层观测;
看似是没有任何问题的,数组中的每一项都非常完美的实现了数据观测;
但是,在 Vue2.x 中,是不支持通过修改数组索引或长度来触发更新的,Why?
备注:Object.defineProperty 支持数组数据类型的劫持;
2,Vue 对性能的权衡
原本可以轻松实现修改数组索引或长度触发更新,,Vue 为什么选择不支持?
这里主要是从框架的性能和应用场景进行权衡考量,最终做出的取舍:
作为一个数组类型,不可避免的会存在大量数据,比如:
let vm = new Vue({
el: '#app',
data() {
return { arr:new Array(9999) }
}
});
按照目前处理逻辑,数组中的 9999 条数据,将全部被添加 get、set 方法
- 为了实现对数组索引的劫持,就需要对数组中每一项进行观测,开销可能会比较大;
- 如果数组使用
Object.defineProperty可以实现修改索引触发更新,但在实际开发场景中,很少会通过arr[888] = x指定索引的方式做数据更新;
所以,权衡性能和应用场景,Vue 源码中没有采用Object.defineProperty对数组进行处理;
备注:这种实现思路,直接导致 vue2 修改数组的索引和长度不能触发视图更新;(在 Vue3 中,数组使用了Object.defineProperty,支持修改索引和长度触发更新)
3,数组劫持的实现思路
对数组进行劫持的核心目标,还是要实现数组的响应式:
- 在 Vue 中,认为这 7 个方法能够改变原数组:push、pop、splice、shift、unshift、reverse、sort;
- 对以上 7 个方法进行特殊处理,使他们能够劫持到数组的数据变化,就能够实现数组的响应式;
对象深层观测的实现:
- 数据观测入口:
src/observe/index.js#observe方法; - 如果数据为对象类型,就会
new Observer实例; - 在 Observer 初始化时,遍历对象中的属性并逐一通过
Object.defineProperty递归处理;
根据之前的分析,数组不能和对象采用相同的处理方式,在 Observer 初始化时会 walk 遍历属性实现递归观测;所以在此处,将数组响应式的处理逻辑单独拆出来,即重写数组的 7 个变异方法;
// src/observe/index.js
import { arrayMethods } from "./array";
class Observer {
constructor(value) {
if(isArray(value)){
// 对数组类型进行单独处理:重写 7 个变异方法
}else{
this.walk(value);
}
}
}
// src/utils
/**
* 判断是否是数组
* @param {*} val
* @returns
*/
export function isArray(val) {
return Array.isArray(val)
}
拉出处理分支后,在 Observer 类中,暂时不会对数组类型进行观测了;
4,数组方法的重写(拦截)思路
这一步的主要工作:数组具有很多原生方法,只重写以上 7 个方法,以实现数组的数据劫持;
注意:仅对响应式数据中的数组进行方法重写,不能影响非响应式数组;
所以,对响应式数据中数组的这 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,关于重写数组方法的理解
- 1,首先,拿到数组的所有原生方法 oldArrayPrototype,通过
Object.create原型继承放到 arrayMethods 原型链上,相当于将原生方法向后移动了一层; - 2,当 value 为数组类型时,修改数组的原型链为 arrayMethods;此时,原本在
value.__proto__上的原生方法,被已换为 arrayMethods 原生方法被向后移动了一层,而中间让出的这一层,就是我们重新 7 个变异方法的地方; - 3,在 arrayMethods 进行处理,在第一层对 7 个变异方法进行重写(此处可以使用策略模式,针对不同方法进行处理和实现),利用 js 原型链查找的机制,就实现了对原生方法的拦截,即重写;
7,数组方法拦截的实现
// src/state.js#initData
function initData(vm) {
let data = vm.$options.data;
data = isFunction(data) ? data.call(vm) : data;
// 在 observe 方法中,当 new Observer 执行完成后,数组的原型方法已被重写
observe(data);
// 测试数组方法的拦截效果
data.arr.push(666);
data.arr.pop()
}
- arrayMethods.push:会在数组自身找到重写的push方法,不会继续到链上查找,实现拦截
- arrayMethods.pop:数组自身没找到重写方法,继续到链上找到原生pop方法
四,结尾
本篇主要介绍了 Vue 数据初始化流程中,数组类型的数据劫持,核心有以下几点:
出于对性能的考虑,Vue 没有对数组类型的数据使用 Object.defineProperty 进行递归劫持,而是通过对能够导致原数组变化的 7 个方法进行拦截和重写实现了数据劫持;
下一篇,数据代理的实现;
维护日志
- 20230109:重构本篇,添加并优化了若干描述和代码注释,使表述更加清晰易懂,更新文章摘要,添加对数组 7 个变异方法的拦截思路;