Vue的响应式系统是其核心特性之一,通过数据劫持完成依赖收集和触发更新,实现了数据变化时自动更新视图的功能。
我们自然地联想到观察者模式:当一个对象的状态发生改变时,所有依赖它的对象都得到通知并自动更新。
其中,数据是被观察者(subject),视图是观察者(observer),视图对象被注册到它依赖的数据对象,当数据变更时会调用视图对象的更新函数。
一、Vue 2 的响应式系统(基于 Object.defineProperty )
Vue2的响应式系统是基于 Object.defineProperty 实现的,通过修改对象属性行为,劫持对data对象属性的读写操作,完成依赖收集和触发更新,从而实现响应式的功能。
1.数据劫持
Vue 2 使用Object.defineProperty修改对象属性行为,劫持data对象属性的读写操作:
- Getter:当访问data对象属性时,收集依赖该属性的视图对象。
- Setter:当修改data对象属性值时,通知依赖该属性的视图对象重新渲染。
2.依赖收集
- 每个data属性维护一个依赖列表,存放依赖该属性的视图对象。
- 当data属性被读取时(组件渲染、watch),触发Getter。
- Getter将当前的视图对象添加到依赖列表中。
3.触发更新
- 当属性值被修改时,Setter会通知依赖该属性的视图重新渲染。
4.核心代码
ini
// 修改对象属性行为,劫持data对象属性的读写操作
function observe(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
});
function defineReactive(obj, key, val) {
const dep = new Dep(); // 依赖管理器
Object.defineProperty(obj, key, {
get() {
// 依赖收集
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
// 触发更新
dep.notify();
}
});
}
kotlin
// 依赖管理器
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
Dep.target = null; // 当前正在渲染的Watcher
// 视图对象
class Watcher {
constructor(vm, expOrFn,cb) {
this.vm = vm;
this.getter = expOrFn; // 渲染函数
this.cb = cb;
this.value = this.get();
}
get() {
Dep.target = this;
const value = this.getter.call(this.vm);
Dep.target = null;
return value;
}
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}
5. Object.defineProperty的不足
Object.defineProperty只能劫持对象属性,无法劫持整个对象,这是其不足之处的根本原因。
- 动态添加的属性不具有响应式
Object.defineProperty只能劫持现有的对象属性,这意味在初始化data对象时,如果某个属性不存在,后续动态添加的属性将不具有响应式功能。
go
const vm = new Vue({ data: { obj: { a: 1 } } });
vm.obj.b = 2; // ❌ 新增属性b,非响应式
delete vm.obj.a; // ❌ 删除属性a,非响应式
// 必须使用 Vue.set/Vue.delete
Vue.set(vm.user, 'gender', 'male')
Vue.delete(vm.user, 'age')
在动态表单场景中,新增或删除表单字段时,无法实现响应式。
- 无法监听数组索引赋值和数组长度的变化
scss
vm.items[0] = 'hi'; // ❌ 索引赋值,非响应式
vm.items.length = 0; // ❌ 修改数组长度,非响应式
// 必须使用vue方法或 Vue.set
vm.list.splice(0, 1, 999) // 正确方式
Vue.set(vm.list, 0, 999) // 正确方式
- 初始化性能
在初始化Vue实例的时候,会递归遍历所有 data 属性,为每个属性设置 getter 和 setter。当处理大对象或深层嵌套结构时会带来严重的性能问题。
- 内存占用
每个data属性都有一个依赖列表,占用了大量的内存。
二、Vue 3的响应式系统(基于Proxy)
Vue3 的响应式系统是基于 ES6 的Proxy API 实现的,相比 Vue2 的Object.defineProperty,它实现了更彻底的响应式能力。其核心原理可以概括为:通过 Proxy 代理目标对象,劫持对象的读、写、删除、添加属性等操作,完成依赖收集和触发更新,从而实现响应式的功能。
1.数据劫持
Vue 3 使用Proxy API创建代理对象,劫持data对象的各种操作,包括读、写、删除、添加属性等操作:
- get:当访问数据时,收集依赖该属性的effect函数。
- set:当修改、新增数据时,通知依赖该属性的effect函数重新计算。
- deleteProperty:当删除数据时,通知依赖该属性的effect函数重新计算。
2.依赖收集
- 维护一个全局的依赖映射表,存放所有响应式对象的所有响应式属性的依赖列表。
- 当属性被读取时(比如在组件渲染、watch),触发get拦截器。
- 将当前的effect函数添加到该属性的依赖列表中。
3.触发更新
当响应式对象的属性被修改、新增或删除时(触发set或deleteProperty拦截器),Vue3 会通知依赖该属性的所有effect函数重新执行,从而实现视图更新或监听响应。
4.核心代码
javascript
// 创建代理对象,劫持data对象的各种操作,包括读、写、删除、新增属性等操作:
function createReactiveObject(target) {
const proxy = 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 (newVal !== val) {
trigger(target, key);
}
return result;
}
// 拦截删除操作
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
trigger(target, key);
return result;
}
});
return proxy;
}
ini
// 全局的依赖映射表:target -> key -> effects
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 => {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
});
}
}
ini
let activeEffect = null;
class ReactiveEffect {
constructor(fn, scheduler = null) {
this.fn = fn;
this.scheduler = scheduler;
}
run() {
activeEffect = this;
const result = this.fn();
activeEffect = null;
return result;
}
}
function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
return _effect;
}