Vue2 与 Vue3 响应式源码对比
引言
在前端开发领域,Vue.js 以其简洁易用和高效的特性受到广泛欢迎。而响应式系统作为 Vue.js 的核心功能之一,它使得数据的变化能够自动更新到视图上,极大地提升了开发效率。Vue2 和 Vue3 在响应式系统的实现上有着显著的差异,本文将深入剖析两者的源码实现,同时探讨 Vue2 存在的问题以及 Vue3 是如何解决这些问题的。
Vue2 响应式源码解析
核心原理
Vue2 的响应式系统基于 Object.defineProperty() 方法。当创建一个 Vue 实例时,Vue 会遍历 data 选项中的所有属性,使用 Object.defineProperty() 将这些属性转换为 getter/setter。这样,当属性值发生变化时,Vue 就能检测到并触发相应的更新操作。
简化源码实现
// 依赖收集器,用于存储依赖该属性的观察者
class Dep {
constructor() {
this.subs = [];
}
// 添加观察者到依赖列表
addSub(sub) {
this.subs.push(sub);
}
// 通知所有观察者数据发生变化
notify() {
this.subs.forEach(sub => sub.update());
}
}
// 观察者,负责监听数据变化并执行相应的回调
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn);
this.value = this.get();
}
// 获取数据的值
get() {
Dep.target = this;
const value = this.getter.call(this.vm, this.vm);
Dep.target = null;
return value;
}
// 数据更新时调用的方法
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}
// 解析路径,用于获取对象的属性值
function parsePath(path) {
const segments = path.split('.');
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return;
obj = obj[segments[i]];
}
return obj;
};
}
// 定义响应式属性
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify();
}
});
}
// 对对象的所有属性进行响应式处理
function observe(obj) {
if (!obj || typeof obj!== 'object') {
return;
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
// 模拟 Vue 实例
class Vue {
constructor(options) {
this.$data = options.data;
observe(this.$data);
new Watcher(this, 'message', (newVal, oldVal) => {
console.log(`数据更新:从 ${oldVal} 到 ${newVal}`);
});
}
}
// 使用示例
const vm = new Vue({
data: {
message: 'Hello, Vue2!'
}
});
// 修改数据触发更新
vm.$data.message = 'New message in Vue2';
解释一下Watcher和Dep的工作流程
在 Vue 2 的响应式系统里,Watcher 和 Dep 互相配合,互相引用,确保数据变化时能及时更新对应的视图。
初始化阶段
- 创建
Dep实例:当 Vue 实例初始化时,会对data选项中的所有属性进行遍历。针对每个属性,都会创建一个Dep(Dependency,依赖收集器)实例。这个实例宛如“小账本”,用于记录哪些Watcher依赖于当前这个属性。 - 创建
Watcher实例:与此同时,Vue 会为需要监听数据变化的地方创建Watcher(观察者)实例(一个vue组件实例被创建时就会创建一个Watcher实例观察这个组件实例的响应式数据)。这些Watcher实例就时刻观察着特定属性,一旦数据发生变化便会迅速执行相应的操作,比如更新视图。需要特别注意的是,Watcher实例并非仅仅在响应式数据创建时才会被创建。在组件里,当使用watch、computed等 API 时,也会调用Watcher类来创建对应的实例进行数据观察。
依赖收集阶段
- 触发
getter方法:在创建Watcher实例的过程中,会触发一次对应数据属性的getter方法。这是因为Watcher需要获取当前数据属性的值,以此作为后续对比和响应的基础。 - 收集依赖:在
getter方法执行时,会检查一个全局变量Dep.target。在创建Watcher实例时,会将当前的Watcher实例赋值给Dep.target。若Dep.target存在,这意味着有Watcher正在获取该属性的值,此时便会把这个Watcher实例(此时该Watcher实例就可以称为一个依赖)添加到当前属性对应的Dep实例的依赖列表中。如此一来,Dep便能清晰知晓有哪些Watcher依赖于这个属性。
数据更新阶段
- 触发
setter方法:当数据属性的值发生变化时,会触发该属性的setter方法。 - 通知依赖更新:在
setter方法中,会调用当前属性对应的Dep实例的notify()方法。这个方法会对Dep实例的依赖列表进行遍历,也就是之前收集到的所有Watcher实例。 Watcher执行更新操作:对于依赖列表中的每个Watcher实例,Dep会调用它们的update()方法。Watcher的update()方法会重新获取数据的新值,并执行预先定义好的回调函数。这个回调函数通常用于更新视图,从而确保界面上显示的数据与实际数据始终保持一致。
总结
Dep 宛如一个高效的“信息中心”,负责收集和管理依赖于某个数据属性的所有 Watcher。而 Watcher 则像一群尽职的“执行者”,监听数据变化并在变化发生时迅速执行相应的更新操作。它们通过依赖收集和通知更新的机制,实现了 Vue 2 中数据的响应式更新。
Vue2 存在的问题
- 对象属性新增和删除无法响应:
Object.defineProperty()只能劫持对象已有的属性,当新增或删除对象的属性时,Vue2 无法自动检测到这些变化。例如:
const vm = new Vue({
data: {
user: {
name: 'John'
}
}
});
// 新增属性,Vue2 无法自动响应
vm.$data.user.age = 25;
- 数组部分操作无法响应:Vue2 对数组的
push()、pop()、shift()等方法进行了重写以实现响应式,但对于直接通过索引修改数组元素或修改数组长度等操作无法自动响应。例如:
const vm = new Vue({
data: {
list: [1, 2, 3]
}
});
// 直接通过索引修改数组元素,Vue2 无法自动响应
vm.$data.list[0] = 10;
- 性能问题:当
data对象非常大时,Object.defineProperty()需要递归遍历对象的所有属性并转换为getter/setter,这会带来较大的性能开销。
Vue3 响应式源码解析
核心原理
Vue3 的响应式系统基于 Proxy 和 Reflect。Proxy 可以对一个对象进行代理,拦截对象的各种操作(如属性访问、赋值、函数调用等);Reflect 提供了一组用于操作对象的方法,与 Proxy 配合使用。当一个对象被 reactive 函数转换为响应式对象时,实际上是创建了一个 Proxy 对象,对该对象的操作都会被 Proxy 拦截,从而实现响应式更新。
简化源码实现
// 依赖收集的全局映射表
const targetMap = new WeakMap();
// 收集依赖
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(activeEffect);
}
// 触发依赖更新
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect());
}
}
// 当前激活的副作用函数
let activeEffect;
// 创建副作用函数
function effect(fn) {
const _effect = function () {
activeEffect = _effect;
fn();
activeEffect = null;
};
_effect();
return _effect;
}
// 创建响应式对象
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (value!== oldValue) {
trigger(target, key);
}
return result;
}
});
}
// 使用示例
const state = reactive({
message: 'Hello, Vue3!'
});
effect(() => {
console.log(state.message);
});
// 修改数据触发更新
state.message = 'New message in Vue3';
Vue 3 的 effect 工作流程
可以看到vue并没有通过Dep和Watcher去进行依赖收集和通知更新,而是通过 effect、track 和 trigger 等机制,实现了更强大、更高效的依赖收集和通知更新功能。
1. 依赖收集阶段
1.1 激活 effect 并设置当前副作用函数
调用 effect 函数并传入回调函数后,该回调函数会被封装成副作用函数。在执行这个副作用函数前,会将其赋值给全局变量 activeEffect,以明确当前正在执行的副作用函数,为后续依赖收集做准备。
1.2 访问响应式数据触发 getter
副作用函数执行过程中,若访问了响应式数据的属性,会触发该响应式对象属性的 getter 方法。这是由于 Vue 3 使用 Proxy 对响应式对象进行代理,拦截了属性的访问操作。
1.3 执行 track 函数收集依赖
在 getter 方法里会调用 track 函数。该函数先检查 activeEffect 是否存在,若存在,则表示有副作用函数正在访问当前属性。接着会在全局的 targetMap(一个 WeakMap)里查找该响应式对象对应的 depsMap(一个 Map),若不存在就创建。然后在 depsMap 里查找该属性对应的依赖集合 dep(一个 Set),若不存在也创建,最后把 activeEffect 添加到这个 dep 集合中,完成依赖收集。
2. 更新执行阶段
2.1 修改响应式数据触发 setter
当响应式数据的属性值发生改变时,会触发该属性的 setter 方法。这同样是因为 Proxy 对属性赋值操作的拦截作用。
2.2 执行 trigger 函数触发更新
在 setter 方法中,若新值与旧值不同,就会调用 trigger 函数。trigger 函数会先从 targetMap 中找到该响应式对象对应的 depsMap,再从中找到该属性对应的依赖集合 dep。然后遍历这个 dep 集合,依次重新执行其中的每个副作用函数,实现数据更新后的副作用函数重新执行,进而更新相关的 DOM 或执行其他副作用操作。
综上所述,effect 通过依赖收集和更新执行的机制,实现了响应式数据变化时副作用函数的自动重新执行,保证了视图或其他副作用操作能及时响应数据的变化。
Vue3 对 Vue2 问题的解决
- 对象属性新增和删除问题:
Proxy可以拦截对象的属性新增和删除操作,所以在 Vue3 中,新增或删除对象的属性都能自动触发响应式更新。例如:
const state = reactive({
user: {
name: 'John'
}
});
// 新增属性,Vue3 可以自动响应
state.user.age = 25;
- 数组操作问题:
Proxy可以拦截数组的各种操作,包括直接通过索引修改数组元素和修改数组长度等,所以在 Vue3 中这些操作都能自动触发响应式更新。例如:
const state = reactive({
list: [1, 2, 3]
});
// 直接通过索引修改数组元素,Vue3 可以自动响应
state.list[0] = 10;
- 性能问题:Vue3 的
Proxy是对整个对象进行代理,不需要像Object.defineProperty()那样递归遍历对象的所有属性,因此在处理大型对象时性能更优。
总结
Vue3 采用 Proxy 和 Reflect 实现的响应式系统解决了 Vue2 中存在的诸多问题,提供了更强大、更灵活的响应式能力,并且在性能上也有显著的提升。所以目前基本上公司的新项目都会使用vue3来开发。