Vue 2 的响应式系统是通过 Object.defineProperty 实现的。Vue 2 的核心思想是通过递归遍历数据对象,对每个属性进行劫持(即使用 Object.defineProperty 定义 getter 和 setter),从而在数据变化时触发依赖更新。
1. 核心概念
- Observer:递归遍历对象的所有属性,将其转换为响应式数据。
- Dep(Dependency) :每个属性都有一个对应的
Dep实例,用于管理依赖(Watcher)。 - Watcher:观察者,负责监听数据变化并执行回调(如更新视图)。
2. 实现步骤
(1)将数据转换为响应式
Vue 2 使用 Object.defineProperty 对对象的每个属性进行劫持。
function defineReactive(obj, key, val) {
const dep = new Dep(); // 每个属性都有一个 Dep 实例
Object.defineProperty(obj, key, {
get() {
if (Dep.target) { // 如果当前有 Watcher 在收集依赖
dep.depend(); // 将 Watcher 添加到 Dep 中
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify(); // 通知所有 Watcher 更新
}
});
}
(2)递归遍历对象
Vue 2 会递归遍历对象的所有属性,将其转换为响应式。
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return;
}
// 遍历对象的所有属性
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
(3)依赖收集(Dep)
Dep 是一个依赖管理器,用于存储所有依赖(Watcher)。
class Dep {
constructor() {
this.subs = []; // 存储 Watcher
}
depend() {
if (Dep.target) {
this.subs.push(Dep.target); // 将当前 Watcher 添加到依赖中
Dep.target.addDep(this); // Watcher 也记录 Dep
}
}
notify() {
this.subs.forEach(watcher => watcher.update()); // 通知所有 Watcher 更新
}
}
Dep.target = null; // 全局变量,指向当前正在收集依赖的 Watcher
(4)观察者(Watcher)
Watcher 是观察者,负责监听数据变化并执行回调。
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
Dep.target = this; // 设置当前 Watcher
this.value = this.vm[this.key]; // 触发 getter,收集依赖
Dep.target = null; // 重置
}
update() {
const newValue = this.vm[this.key];
if (newValue !== this.value) {
this.value = newValue;
this.cb(newValue); // 执行回调
}
}
}
(5)初始化响应式数据
在 Vue 实例化时,会调用 observe 将数据转换为响应式。
class Vue {
constructor(options) {
this._data = options.data;
observe(this._data); // 将数据转换为响应式
}
}
3. 数组的响应式处理
Vue 2 对数组的处理与对象不同,因为 Object.defineProperty 无法直接监听数组的变化(如 push、pop 等操作)。Vue 2 通过重写数组的原型方法来实现数组的响应式。
(1)重写数组方法
Vue 2 创建了一个新的数组原型,并重写了会改变数组的方法(如 push、pop、splice 等)。
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
value: function(...args) {
const result = original.apply(this, args);
const ob = this.__ob__; // 获取 Observer 实例
ob.dep.notify(); // 通知依赖更新
return result;
},
enumerable: false,
writable: true,
configurable: true
});
});
(2)将数组转换为响应式
在 observe 函数中,如果检测到是数组,则将其原型指向重写后的 arrayMethods。
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return;
}
if (Array.isArray(obj)) {
obj.__proto__ = arrayMethods; // 重写数组原型
obj.forEach(item => observe(item)); // 递归处理数组元素
} else {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
}
4. 总结
Vue 2 的响应式系统核心是通过 Object.defineProperty 对对象的属性进行劫持,并通过 Dep 和 Watcher 实现依赖收集和更新。对于数组,Vue 2 通过重写数组的原型方法来实现响应式。
优点:
- 实现简单,兼容性好(支持 IE9+)。
- 对对象的属性劫持非常精细。
缺点:
- 无法监听新增或删除的属性(需要使用
Vue.set或Vue.delete)。 - 对数组的处理需要额外逻辑。
在 Vue 3 中,响应式系统改用了 Proxy,解决了 Vue 2 中的一些局限性。