公司旧项目在使用vue2开发,管用react-hooks的我研究了一下它的底层原理,让我们更详细地解释 Vue 2 的 响应式原理,特别是
Object.defineProperty是如何实现数据与视图之间的自动关联的。Vue 的响应式系统是其核心特性之一,它使得当数据发生变化时,视图能够自动更新,而无需手动操作 DOM。
1. 什么是响应式?
响应式 指的是当数据发生变化时,系统能够自动感知并做出相应的反应。在 Vue 中,这意味着当你修改了 data 中的某个属性时,Vue 会自动更新所有依赖于这个属性的视图部分。
2. Vue 2 的响应式实现原理
Vue 2 的响应式系统主要依赖于 Object.defineProperty。它的核心思想是通过 数据劫持 和 依赖收集 来实现数据与视图的自动绑定。
步骤详解
步骤 1:数据劫持(Data Observation)
Vue 在初始化时,会遍历 data 对象中的所有属性,并使用 Object.defineProperty 将这些属性转换为 getter 和 setter。这样,每当访问或修改这些属性时,Vue 都能捕获到这些操作。
function defineReactive(obj, key, val) {
// 每个属性都有一个依赖收集器(Dep)
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true, // 属性可枚举
configurable: true, // 属性可配置
get: function reactiveGetter() {
// 收集依赖:当前属性被访问时,记录下哪些 Watcher 需要监听它
if (Dep.target) {
dep.depend();
}
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) return; // 如果值没有变化,不做任何操作
val = newVal;
// 触发更新:通知所有依赖该属性的 Watcher 进行更新
dep.notify();
}
});
}
// 示例
const data = { message: 'Hello Vue!' };
defineReactive(data, 'message', data.message);
data.message; // 访问属性,触发 getter,收集依赖
data.message = 'Hello World!'; // 修改属性,触发 setter,通知更新
步骤 2:依赖收集(Dependency Collection)
在 Vue 中,视图渲染的过程是通过 Watcher 来完成的。Watcher 是一个观察者,它负责监听数据的变化并更新视图。
当 Vue 渲染组件时,会访问 data 中的属性。在访问这些属性时,getter 被触发,Vue 会将当前的 Watcher 添加到该属性的依赖列表中(即 Dep)。这个过程称为 依赖收集。
class Dep {
constructor() {
this.subscribers = new Set(); // 存储 Watcher
}
depend() {
if (Dep.target) {
this.subscribers.add(Dep.target);
}
}
notify() {
this.subscribers.forEach(watcher => watcher.update());
}
}
Dep.target = null; // 全局变量,指向当前的 Watcher
class Watcher {
constructor(updateCallback) {
Dep.target = this; // 设置当前 Watcher
this.updateCallback = updateCallback;
this.update(); // 触发更新,从而进行依赖收集
Dep.target = null; // 重置
}
update() {
this.updateCallback();
}
}
// 示例
const data = {};
defineReactive(data, 'message', 'Hello Vue!');
new Watcher(() => {
console.log(`视图更新:${data.message}`);
});
data.message = 'Hello World!'; // 修改数据,触发视图更新
解释:
-
Watcher 在初始化时会执行
update方法,这个方法会访问data.message。 - 访问
data.message时,getter 被触发,当前的 Watcher 被添加到Dep的订阅列表中。 - 当
data.message被修改时,setter 被触发,Dep通知所有订阅的 Watcher 进行更新。
步骤 3:派发更新(Dispatching Updates)
当数据发生变化时,setter 不仅更新值,还会通知所有依赖该属性的 Watcher。Watcher 收到通知后,会执行相应的回调函数,通常是重新渲染视图。
// 继续上面的示例
data.message = 'Hello Vue 3!'; // 修改数据
// 输出:视图更新:Hello Vue 3!
3. 响应式系统的完整流程
结合上述步骤,Vue 2 的响应式系统的工作流程如下:
初始化阶段:
• Vue 遍历data对象,使用Object.defineProperty将每个属性转换为 getter 和 setter。
• 每个属性都有一个对应的Dep实例,用于存储依赖(Watcher)。渲染阶段:
• Vue 渲染组件时,会访问data中的属性。
• 访问属性时,getter 被触发,当前的 Watcher 被添加到Dep的订阅列表中。数据变化阶段:
• 当数据发生变化时,setter 被触发。
• setter 通知Dep,Dep遍历所有订阅的 Watcher,并调用它们的update方法。视图更新阶段:
• Watcher 的update方法被调用,通常会重新渲染组件,更新视图。
4. 示例代码:完整的响应式系统
以下是一个简化的示例,展示了 Vue 2 响应式系统的核心部分:
// 依赖收集器
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (Dep.target) {
this.subscribers.add(Dep.target);
}
}
notify() {
this.subscribers.forEach(watcher => watcher.update());
}
}
Dep.target = null;
// Watcher 类
class Watcher {
constructor(vm, key, updateCallback) {
this.vm = vm;
this.key = key;
this.updateCallback = updateCallback;
Dep.target = this; // 设置当前 Watcher
this.value = vm[key]; // 访问属性,触发 getter,进行依赖收集
Dep.target = null; // 重置
}
update() {
const newValue = this.vm[this.key];
this.updateCallback(newValue);
}
}
// 定义响应式属性
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
dep.depend();
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
dep.notify();
}
}
});
}
// 示例使用
const data = {};
defineReactive(data, 'message', 'Hello Vue!');
new Watcher(data, 'message', (newValue) => {
console.log(`视图更新:${newValue}`);
});
console.log(data.message); // 输出:Hello Vue!
data.message = 'Hello World!'; // 输出:视图更新:Hello World!
data.message = 'Hello Vue 3!'; // 输出:视图更新:Hello Vue 3!
解释:
• Dep 类用于管理依赖(Watcher)。
• Watcher 类表示一个观察者,当依赖的数据变化时,会执行回调函数。
• defineReactive 函数使用 Object.defineProperty 将对象的属性转换为响应式属性。
• 当 data.message 被访问时,依赖被收集;当被修改时,通知所有 Watcher 进行更新。
5. Vue 2 响应式系统的局限性
虽然 Object.defineProperty 能够实现基本的响应式功能,但它也有一些局限性:
无法监听对象属性的新增和删除:
• Vue 2 只能在初始化时劫持已有的属性。如果后续动态添加或删除属性,Vue 无法自动捕获这些变化。
• 解决方法:使用Vue.set和Vue.delete方法。无法监听数组的变化:
• 直接通过索引修改数组元素,或者修改数组的长度,Vue 无法检测到这些变化。
• 解决方法:Vue 对数组的变异方法(如push、pop、splice等)进行了重写,使得这些方法能够触发视图更新。性能开销:
• 对于大型数据结构,递归地进行响应式转换可能会带来一定的性能开销。
6. Vue 3 的改进
Vue 3 引入了 Proxy 来替代 Object.defineProperty,Proxy 能够更灵活地拦截对象的操作,包括属性的新增、删除、数组的变化等,从而解决了 Vue 2 中的一些局限性,并提升了性能。