Appearance
Vue3 响应式变化 ✨
要点速览
- 拦截机制:
Proxy全对象拦截,集合类型可感知;ref以 getter/setter 包裹原始值,reactive返回代理。 - 创建响应式:
ref/reactive/shallowReactive/shallowRef/readonly;toRaw/markRaw管理原始对象与非响应对象。 - 依赖追踪:
WeakMap → Map → Set精细到属性级别;track/trigger驱动副作用;effect支持调度与停止。 - 最佳实践:避免解构丢失响应性,使用
toRefs/toRef;派生数据用computed,副作用用watch/watchEffect。
变化总览
Vue3 的响应式与 Vue2 相比主要变更集中在三处:
- 数据拦截:由
Object.defineProperty(属性级)升级为Proxy(对象级),覆盖新增/删除属性、原型、集合类型等更多操作。 - 创建响应式:从
data()的选项式初始化,转向ref/reactive家族的组合式声明;更清晰的原始值与对象响应模型。 - 依赖收集:从
Watcher + Dep(以组件/渲染为单位)到WeakMap/Map/Set(属性级副作用函数),更新粒度更细、调度更灵活。
数据拦截
差异与能力边界
- Vue2:
Object.defineProperty只能拦截已存在属性的读写,无法直接感知新增/删除属性、原型链变更、数组长度变化等;对Map/Set等集合类型支持不足。 - Vue3:
Proxy拦截整个对象的操作(get/set/deleteProperty/has/ownKeys/getPrototypeOf/setPrototypeOf等),天然覆盖属性新增/删除、原型访问、数组方法与集合类型操作。 - 性能:在多数场景下
Proxy更高效;同时减少了对深层递归定义拦截的需求。
js
// 基本代理处理思路(简化示意)
const reactiveHandlers = {
get(target, key, receiver) {
track(target, key);
const res = Reflect.get(target, key, receiver);
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receiver) {
const old = target[key];
const ok = Reflect.set(target, key, value, receiver);
if (ok && old !== value) trigger(target, key);
return ok;
},
deleteProperty(target, key) {
const ok = Reflect.deleteProperty(target, key);
if (ok) trigger(target, key);
return ok;
},
};创建响应式
API 与适用场景
ref(value):封装原始值或对象的响应引用,通过.value读写;在模板中自动解包。reactive(obj):返回对象的代理;应对嵌套结构更自然;不适用于原始值。shallowRef/shallowReactive:仅浅层追踪,适合大型不可变结构或第三方实例。readonly(obj):只读代理,约束外部不可修改;配合模块边界更安全。toRaw/markRaw:获取原始对象或标记对象为非响应,避免不必要的追踪与开销。
部分源码
js
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(
value: T,
public readonly __v_isShallow: boolean,
) {
this._rawValue = __v_isShallow ? value : toRaw(value)
// 有可能是原始值,有可能是 reactive 返回的 proxy
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
// 收集依赖 略
return this._value
}
set value(newVal) {
// 略
}
}
// 判断是否是对象,是对象就用 reactive 来处理,否则返回原始值
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : valuejs
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// ...
// 创建 Proxy 代理对象
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
);
proxyMap.set(target, proxy);
return proxy;
}
export function reactive(target: object) {
// ...
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
);
}依赖收集
Vue2:Watcher + Dep
- 每个响应式属性都有一个
Dep实例,用于做依赖收集,内部包含了一个Set,存储依赖这个属性的所有Watcher - 当属性值发生变化,
Dep就会通知所有的Watcher去做更新操作
- 每个响应式属性都有一个
示例(Vue2 的 Dep/Watcher 思路)
js
// Dep:依赖收集器,管理依赖当前属性的所有观察者(Watcher)
class Dep {
constructor() {
// 使用 Set 去重,避免同一观察者被重复收集
this.subs = new Set();
}
// 在属性的 getter 中调用,完成依赖收集
depend() {
if (Dep.target) this.subs.add(Dep.target);
}
// 在属性的 setter 中调用,通知所有观察者更新
notify() {
this.subs.forEach((w) => w.update());
}
}
// defineReactive:为指定对象属性定义响应式的 get/set 劫持
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
// 读取属性时,收集当前处于激活状态的观察者(Dep.target)
get() {
dep.depend();
return val;
},
// 写入属性时,如果值变更则通知依赖该属性的所有观察者重新计算
set(newVal) {
if (newVal !== val) {
val = newVal;
dep.notify();
}
},
});
return dep;
}
// Watcher:观察者,封装了一个 getter,用于读取依赖的响应属性
class Watcher {
constructor(getter) {
this.getter = getter;
this.value = undefined;
// 立即求值并完成依赖收集(触发被依赖属性的 getter)
this.get();
}
// 激活当前观察者,执行 getter 以收集依赖,并保存当前值
get() {
Dep.target = this;
this.value = this.getter();
Dep.target = null;
return this.value;
}
// 当依赖的属性发生变更时由 Dep 调用,重新求值
update() {
this.get();
}
}
// 示例:为 state.count 定义响应式属性,并创建两个观察者
const state = {};
defineReactive(state, "count", 0);
// 观察者 w1:打印 count 的原值
const w1 = new Watcher(() => {
console.log("w1", state.count);
return state.count;
});
// 观察者 w2:打印 count 的两倍
const w2 = new Watcher(() => {
console.log("w2", state.count * 2);
return state.count * 2;
});
// 修改 count,会触发 dep.notify(),从而依次调用 w1.update()/w2.update()
state.count = 1;
state.count = 2;核心机制概述:
- 通过
Object.defineProperty拦截属性的get/set。 - 每个属性对应一个
Dep,内部用Set保存依赖当前属性的Watcher。 - 读取属性(get)时,若存在激活的
Dep.target,则收集到dep.subs。 - 修改属性(set)且新值不同,触发
dep.notify(),依次调用Watcher.update()重新运行其 getter。
运行流程:
new Watcher(getter):设置自身为Dep.target并立即执行getter完成依赖收集。- 之后对
state.count的读取会被收集依赖;写入则通知相关Watcher更新并重新计算。
- Vue3:WeakMap + Map + Set
- Vue3 的依赖收集粒度更细
- WeakMap 键对应的是响应式对象,值是一个 Map,这个 Map 的键是该对象的属性,值是一个 Set,Set 里面存储了所有依赖于这个属性的 effect 函数
示例(Vue3 的 WeakMap/Map/Set + effect 思路)
js
const targetMap = new WeakMap();
let activeEffect = null;
function effect(fn) {
const eff = () => {
activeEffect = eff;
fn();
activeEffect = null;
};
eff();
return eff;
}
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);
dep && dep.forEach((eff) => eff());
}
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key);
return res;
},
set(target, key, value, receiver) {
const old = target[key];
const ok = Reflect.set(target, key, value, receiver);
if (ok && old !== value) trigger(target, key);
return ok;
},
});
}
const state = reactive({ count: 0 });
const e1 = effect(() => {
console.log("e1", state.count);
});
const e2 = effect(() => {
console.log("e2", state.count * 2);
});
state.count = 1;
state.count = 2;核心机制概述:
- 使用
Proxy/Reflect拦截对象属性的get/set,实现依赖收集与触发更新。 - 依赖存储结构:
WeakMap(target) -> Map(key) -> Set(effects)。 - 全局变量
activeEffect指向当前执行的副作用函数;在effect(fn)执行期间,读取被拦截属性会调用track收集依赖。 - 属性写入成功且新旧值不同时调用
trigger,按Set中的副作用逐个重新执行,实现响应式更新。
运行流程:
- 调用
effect(fn):设置activeEffect并立即执行 fn 完成依赖收集(首轮运行)。 - 读取属性:触发
track,将activeEffect记录到对应的依赖集合。 - 写入属性:触发
trigger,取出依赖集合并逐个执行副作用函数,驱动视图或计算更新。
总结起来,Vue3 相比 Vue2 的依赖追踪粒度更细。Vue2 中 Dep 收集的是 Watcher 实例(如组件渲染 watcher、计算属性 watcher 等),而 Vue3 中收集的是属性级的副作用函数(effect),并以 WeakMap(target) → Map(key) → Set(effect) 的结构组织依赖。
面试题
说一说 Vue3 响应式相较于 Vue2 是否有改变?如果有,那么说一下具体有哪些改变?
参考答案:
相比较 Vue2,Vue3 在响应式的实现方面有这么一些方面的改变:
- 数据拦截从 Object.defineProperty 改为了 Proxy + Object.defineProperty 的拦截方式,其中
- ref:使用 ObjectdefineProperty + Proxy 方式
- reactive:使用 Proxy 方式
- 创建响应式数据在语法层面有了变化:
- Vue2: 通过 data 来创建响应式数据
- Vue3: 通过 ref、reactvie 等方法来创建响应式数据
- 依赖收集上面的变化
- Vue2:Watcher + Dep
- Vue3:WeakMap + Map + Set
- 这种实现方式可以实现更细粒度的依赖追踪和更新控制
