vue2.x中的数据劫持
数据劫持(Data Hijacking)
- 数据劫持是一种核心技术,用于监听数据的变化,然后根据这些变化触发相应的操作。
- 在 Vue 中,数据劫持 是一种通过拦截对数据的访问和修改,从而实现数据绑定和响应式更新的技术。Vue 利用这一机制,使得当数据发生变化时,视图能够自动更新,从而实现 MVVM(Model-View-ViewModel)架构的双向绑定。
Vue2.x Object 的数据劫持
Object的数据劫持步骤
- 使用 Object.defineProperty()
- 目的:定义或修改对象的属性。
- 做法:Vue 利用 Object.defineProperty() 来定义(或修改)对象的属性,这样就可以在属性被访问或修改时,执行一些额外的逻辑(如通知更新)。
- 示例:
let obj = { foo: 'bar' };
Object.defineProperty(obj, 'foo', {
configurable: true, // 是否可以配置(修改属性 descriptor)
enumerable: true, // 是否可以枚举(for...in、Object.keys())
get: function() { // 获取属性时调用
console.log('获取 foo 属性');
return 'bar';
},
set: function(newValue) { // 设置属性时调用
console.log('更新 foo 属性为:' + newValue);
// 在这里,Vue 会进行依赖通知,触发更新
}
});
- 递归劫持
- 目的:确保对象的所有嵌套属性也被劫持。
- 做法:Vue会递归遍历对象的所有属性,如果该属性的值也是对象,则对其也进行数据劫持。这确保了无论对象嵌套多深,数据变化都能被检测到。
Vue 2.x 源码解析
文件:vue/src/core/observer/index.js (Vue 2.6.12)
关键源码片段
/**
* 观察者(Observer)构造函数
* @param {Object} value - 需要被观察的对象
*/
export class Observer {
constructor(value) {
this.value = value; // 被观察对象
this.dep = new Dep(); // 依赖管理器
this.vmCount = 0; // 统计被多少个 Vue 实例使用
// 定义不可枚举的 __ob__ 属性,指向当前 Observer 实例
def(value, '__ob__', this);
// 如果是数组,走数组处理逻辑
if (Array.isArray(value)) {
// ...
} else {
// 递归定义对象属性
this.walk(value);
}
}
/**
* 递归定义对象属性
* @param {Object} obj - 被观察对象
*/
walk(obj) {
const keys = Object.keys(obj); // 获取对象键数组
for (let i = 0; i < keys.length; i++) {
// 定义响应式属性
defineReactive(obj, keys[i]);
}
}
}
/**
* 定义响应式属性
* @param {Object} obj - 被观察对象
* @param {string} key - 属性键
* @param {any} val - 属性值
*/
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep(); // 依赖管理器
// 递归观察子对象
let childOb = !shallow && observe(val);
// 使用 Object.defineProperty 定义属性
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可配置
get: function reactiveGetter() {
// 获取属性时
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
// 依赖收集
dep.depend();
if (childOb) {
dependArray(value);
}
}
return value;
},
set: function reactiveSetter(newVal) {
// 设置属性时
const value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
// 新值观察
childOb = !shallow && observe(newVal);
// 通知更新
dep.notify();
}
});
}
Observer 类
- 构造函数:
- value: 被观察的对象。
- dep: 依赖管理器,用于管理订阅该对象的依赖。
- vmCount: 统计被多少个 Vue 实例使用。
- def(value, ‘_ob_’, this): 定义不可枚举的 _ob_ 属性,指向当前 Observer 实例,以便在其他地方可以访问到该实例。
- 如果 value 是数组,则会处理数组的响应式逻辑(此处未实现)。
- 如果 value 是对象,则调用 walk 方法递归地定义对象的每个属性的响应式特性。
- walk 方法:
- 获取对象的所有键,然后遍历这些键,调用 defineReactive 方法为每个属性定义响应式特性。
defineReactive 函数
- 参数:
- obj: 被观察的对象。
- key: 对象的属性键。
- val: 属性值。
- customSetter: 自定义的 setter 函数。
- shallow: 是否浅层观察,如果不为真,则递归观察子对象。
- 依赖管理:
- 创建一个 dep 实例用于管理该属性的依赖。
- 如果属性值是一个对象且不是浅层观察,则递归调用 observe 函数来观察子对象。
- 属性描述符:
- 使用 Object.defineProperty 来重新定义对象的属性,使其具备响应式特性。
- getter:当属性被读取时,收集依赖。如果属性值是一个对象,则继续收集子对象的依赖。
- setter:当属性被设置时,如果新值和旧值不同,则更新值,并通知所有依赖进行更新。
依赖收集和通知
- 依赖收集:当属性被读取时,如果存在 Dep.target(表示有依赖正在收集),则调用 dep.depend() 进行依赖收集,同时对子对象进行依赖收集。
- 通知更新:当属性被设置时,通知所有订阅该属性的依赖进行更新,调用 dep.notify()。
Vue2.x Array 的数据劫持
由于Array也是对象的一种(特殊的对象),理论上可以使用Object.defineProperty()来劫持。但是,数组有一些特殊的方法(如push、pop、splice等),直接使用defineProperty劫持可能不足以捕获所有变化。
Array劫持策略
- 重写数组原型方法
- 目的:拦截数组方法调用,以检测变化并通知更新。
- 做法:Vue 重写了数组的原型方法(如push、pop、shift、unshift、splice、sort、reverse),在这些方法内部,除了执行原有的逻辑外,还会主动触发依赖更新。
- 使用 Object.defineProperty() 劫持数组长度
- 目的:监听数组长度的变化,因为数组长度的变化可能是通过索引直接修改数组元素导致的。
- 做法:通过Object.defineProperty()劫持数组的length属性,以捕获通过索引修改数组长度的情况。
文件:vue-next/src/reactivity/reactive.ts
关键源码片段
// 缓存原始数组原型方法
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
// 重写数组原型方法
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(function(method) {
// 缓存原始方法
const original = arrayProto[method];
// 定义新的方法
def(arrayMethods, method, function mutator(...args) {
// 调用原始方法
const result = original.apply(this, args);
const ob = this.__ob__;
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted);
// 通知更新
ob.dep.notify();
return result;
});
});
-
缓存原始数组原型方法
- arrayProto:引用了 JavaScript 的原生数组原型 Array.prototype,这是为了保留数组的原始方法,不直接修改它。
- arrayMethods:创建了一个以 Array.prototype 为原型的新对象,用来定义自定义的数组方法。通过 Object.create(),arrayMethods 继承了 Array.prototype 的所有方法。
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
- 重写数组原型方法
列出常见的可以修改数组内容的方法。包括:- push:向数组末尾添加元素。
- pop:从数组末尾移除元素。
- shift:从数组开头移除元素。
- unshift:向数组开头添加元素。
- splice:添加或移除数组中的元素。
- sort:对数组进行排序。
- reverse:反转数组的顺序。
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(function(method) {
- 缓存原始方法并定义新的方法
- original:保存了对应的数组方法的原始实现,比如 push、pop 等。
- def():假设这是一个用于定义属性的工具函数,可能类似于 Object.defineProperty,它将新的自定义方法定义在 arrayMethods 对象上。
- mutator(…args):这是一个新的方法,它会替代原来的数组方法。当你在数组上调用 push 等方法时,实际上调用的是这个 mutator 函数。
- original.apply(this, args):在新的方法中,仍然会调用原始的数组方法,并将 this 和参数传递进去。这保证了数组的原始行为不会被改变。
- 处理新插入的元素
- ob:假设 this 是一个被观察的数组实例,而 _ob_ 是该数组的观察者对象。这个对象通常包含了观察功能,比如响应式数据系统中的 Observer 实例。
- inserted:用于保存新插入到数组中的元素。push 和 unshift 方法会直接把所有传入的参数当作新元素;splice 则从第三个参数开始表示新插入的元素(args.slice(2))。
- ob.observeArray(inserted):如果有新插入的元素,调用 observeArray 方法对这些新元素进行观察。这个过程通常会递归地将新元素也变成响应式的,确保数组中的每个元素都能被追踪变化。
const ob = this.__ob__;
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted);
-
通知依赖更新
- ob.dep.notify():通知依赖于这个数组的其他部分(比如视图层)发生了变化。这个 dep 通常是依赖管理系统中的一个实例,用于触发依赖的重新计算或视图的重新渲染。
- return result:最后返回原始方法的执行结果,保证方法的正常功能不受影响。
ob.dep.notify();
return result;
总结
- Vue 2.x:
- Object劫持:主要通过Object.defineProperty()递归定义(或修改)对象属性,用于捕获属性的读写操作。
- Array劫持:采用了重写数组原型方法和定义length属性的方式,以确保捕获到数组所有可能的变化操作。