Vue 响应式系统
概述
Vue 3 使用 Proxy 实现响应式系统,是整个框架的核心。理解响应式原理对于掌握 Vue 至关重要。
一、Vue 2 vs Vue 3 响应式对比
| 特性 | Vue 2 (defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 数组监听 | 需要重写数组方法 | 原生支持 |
| 新增属性 | 需要 $set | 自动响应 |
| 删除属性 | 需要 $delete | 自动响应 |
| 性能 | 初始化递归遍历 | 惰性代理 |
| Map/Set | 不支持 | 支持 |
| 兼容性 | IE9+ | 不支持 IE |
二、Proxy 基础
Proxy 拦截操作
javascript
const target = { name: 'Vue' };
const proxy = new Proxy(target, {
// 读取属性
get(target, key, receiver) {
console.log(`get ${key}`);
return Reflect.get(target, key, receiver);
},
// 设置属性
set(target, key, value, receiver) {
console.log(`set ${key} = ${value}`);
return Reflect.set(target, key, value, receiver);
},
// 删除属性
deleteProperty(target, key) {
console.log(`delete ${key}`);
return Reflect.deleteProperty(target, key);
},
// 检查属性
has(target, key) {
console.log(`has ${key}`);
return Reflect.has(target, key);
},
// 获取所有 key
ownKeys(target) {
console.log('ownKeys');
return Reflect.ownKeys(target);
}
});
proxy.name; // get name
proxy.age = 18; // set age = 18
delete proxy.age; // delete age
'name' in proxy; // has name
Object.keys(proxy); // ownKeys为什么使用 Reflect?
javascript
const obj = {
_name: 'Vue',
get name() {
return this._name; // this 指向很重要
}
};
const proxy = new Proxy(obj, {
get(target, key, receiver) {
// ❌ 直接访问 target,getter 中的 this 是原对象
// return target[key];
// ✅ 使用 Reflect,getter 中的 this 是 proxy
return Reflect.get(target, key, receiver);
}
});三、响应式核心实现
核心 API
javascript
// reactive - 对象响应式
const state = reactive({ count: 0 });
// ref - 基本类型响应式
const count = ref(0);
count.value++; // 需要 .value
// shallowReactive - 浅层响应式
const state = shallowReactive({ nested: { a: 1 } });
state.nested = { a: 2 }; // 响应式
state.nested.a = 3; // 非响应式
// readonly - 只读代理
const state = readonly({ count: 0 });
state.count++; // 警告,不生效
// shallowReadonly - 浅层只读reactive 实现
javascript
// 缓存已代理对象
const reactiveMap = new WeakMap();
function reactive(target) {
// 只处理对象
if (typeof target !== 'object' || target === null) {
return target;
}
// 已经是响应式对象
if (reactiveMap.has(target)) {
return reactiveMap.get(target);
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 特殊标记
if (key === '__v_isReactive') return true;
const result = Reflect.get(target, key, receiver);
// 依赖收集
track(target, key);
// 深层响应式(惰性)
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 值变化时触发更新
if (oldValue !== value) {
trigger(target, key);
}
return result;
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
trigger(target, key);
}
return result;
}
});
reactiveMap.set(target, proxy);
return proxy;
}ref 实现
javascript
function ref(value) {
return new RefImpl(value);
}
class RefImpl {
constructor(value) {
this._value = isObject(value) ? reactive(value) : value;
this.__v_isRef = true;
}
get value() {
track(this, 'value');
return this._value;
}
set value(newValue) {
if (newValue !== this._value) {
this._value = isObject(newValue) ? reactive(newValue) : newValue;
trigger(this, 'value');
}
}
}
// 自动解包
function unref(ref) {
return isRef(ref) ? ref.value : ref;
}
// 在模板中自动解包
// template: {{ count }} 而不是 {{ count.value }}四、依赖收集与派发更新
核心数据结构
targetMap: WeakMap {
target -> depsMap: Map {
key -> dep: Set [ effect1, effect2, ... ]
}
}依赖收集 (track)
javascript
let activeEffect = null;
const targetMap = new WeakMap();
function track(target, key) {
if (!activeEffect) return;
// 获取或创建 depsMap
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 获取或创建 dep
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 添加当前 effect
dep.add(activeEffect);
// 双向记录,用于清理
activeEffect.deps.push(dep);
}派发更新 (trigger)
javascript
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (!dep) return;
// 创建新 Set 避免无限循环
const effectsToRun = new Set();
dep.forEach(effect => {
// 避免无限递归
if (effect !== activeEffect) {
effectsToRun.add(effect);
}
});
effectsToRun.forEach(effect => {
// 调度器
if (effect.scheduler) {
effect.scheduler(effect);
} else {
effect.run();
}
});
}effect 实现
javascript
function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
// 立即执行一次
if (!options.lazy) {
_effect.run();
}
// 返回 runner
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
}
class ReactiveEffect {
constructor(fn, scheduler) {
this.fn = fn;
this.scheduler = scheduler;
this.deps = [];
this.active = true;
}
run() {
if (!this.active) {
return this.fn();
}
// 清理旧依赖
cleanup(this);
// 设置当前 effect
activeEffect = this;
// 执行函数,触发依赖收集
const result = this.fn();
// 重置
activeEffect = null;
return result;
}
stop() {
if (this.active) {
cleanup(this);
this.active = false;
}
}
}
function cleanup(effect) {
effect.deps.forEach(dep => {
dep.delete(effect);
});
effect.deps.length = 0;
}五、computed 实现
javascript
function computed(getterOrOptions) {
let getter, setter;
if (typeof getterOrOptions === 'function') {
getter = getterOrOptions;
setter = () => console.warn('computed is readonly');
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
return new ComputedRefImpl(getter, setter);
}
class ComputedRefImpl {
constructor(getter, setter) {
this._dirty = true; // 脏值标记
this._value = undefined;
this._setter = setter;
// 创建 effect,使用 scheduler
this.effect = new ReactiveEffect(getter, () => {
// 依赖变化时,标记为脏
if (!this._dirty) {
this._dirty = true;
trigger(this, 'value');
}
});
}
get value() {
track(this, 'value');
// 脏值时重新计算
if (this._dirty) {
this._value = this.effect.run();
this._dirty = false;
}
return this._value;
}
set value(newValue) {
this._setter(newValue);
}
}computed 特性
javascript
const count = ref(0);
const double = computed(() => {
console.log('computed');
return count.value * 2;
});
// 1. 惰性计算
console.log(double.value); // 'computed', 0
console.log(double.value); // 0 (使用缓存)
// 2. 依赖变化后重新计算
count.value = 1;
console.log(double.value); // 'computed', 2六、watch 实现
javascript
function watch(source, cb, options = {}) {
let getter;
if (typeof source === 'function') {
getter = source;
} else if (isRef(source)) {
getter = () => source.value;
} else if (isReactive(source)) {
getter = () => traverse(source); // 深度遍历
}
let oldValue;
const job = () => {
const newValue = effect.run();
if (newValue !== oldValue) {
cb(newValue, oldValue);
oldValue = newValue;
}
};
const effect = new ReactiveEffect(getter, job);
if (options.immediate) {
job();
} else {
oldValue = effect.run();
}
// 返回 stop 函数
return () => effect.stop();
}
// 深度遍历
function traverse(value, seen = new Set()) {
if (typeof value !== 'object' || value === null || seen.has(value)) {
return value;
}
seen.add(value);
for (const key in value) {
traverse(value[key], seen);
}
return value;
}七、响应式边界情况
数组处理
javascript
const arr = reactive([1, 2, 3]);
// 索引访问 - 响应式
arr[0] = 10;
// length 变化 - 响应式
arr.length = 2;
// 数组方法 - 需要特殊处理
arr.push(4); // 会触发 get('push'), get('length'), set('3')集合类型处理
javascript
// Map
const map = reactive(new Map());
map.set('key', 'value'); // 需要拦截 set 方法
// Set
const set = reactive(new Set());
set.add(1); // 需要拦截 add 方法
// 实现要点:拦截 get 方法,返回绑定的函数面试高频题
Q1: Vue 3 为什么用 Proxy 替代 defineProperty?
答:
- 数组支持: Proxy 可以监听数组索引和 length 变化
- 新增/删除属性: 无需 $set/$delete
- 性能: 惰性代理,不需要初始化递归
- 更多拦截: has, ownKeys, deleteProperty 等
Q2: ref 和 reactive 的区别?
答:
| ref | reactive |
|---|---|
| 包装基本类型 | 只接受对象 |
| 需要 .value | 直接访问 |
| 可以重新赋值整体 | 不能重新赋值整体 |
| 返回 RefImpl | 返回 Proxy |
Q3: computed 和 watch 的区别?
答:
| computed | watch |
|---|---|
| 有返回值 | 无返回值 |
| 缓存结果 | 不缓存 |
| 同步计算 | 可异步 |
| 不能有副作用 | 专门处理副作用 |
Q4: 为什么要用 WeakMap 存储依赖?
答: WeakMap 的 key 是弱引用,当 target 对象被回收时,对应的依赖也会被自动回收,避免内存泄漏。