一、前言
响应式是 Vue 核心特性之一,它让数据变更自动驱动视图更新,极大简化了前端开发的状态管理。Vue 3 彻底重构了响应式系统,摒弃了 Vue 2 的 Object.defineProperty,转而基于 ES6 Proxy 实现,不仅解决了旧版响应式的缺陷,还带来了更强大、更灵活的响应式能力。
本文将从核心痛点、实现原理、源码逻辑、手写简易版四个维度,彻底拆解 Vue 3 响应式原理。
二、Vue 2 响应式的痛点(为何重构)
Vue 2 基于 Object.defineProperty 劫持对象属性的 get/set,存在天然局限:
- 无法监听对象新增 / 删除属性 :需借助
Vue.set/Vue.delete - 无法监听数组下标赋值、长度修改:需重写数组方法(push/pop 等)
- 递归劫持深度对象,性能开销大
- 仅支持对象类型,对 Set/Map 等新数据结构无能为力
Vue 3 用 Proxy 完美解决了这些问题,同时支持对象、数组、Set、Map、WeakMap、WeakSet全类型响应式。
三、Vue 3 响应式核心 API
Vue 3 提供了分层响应式 API,适配不同场景:
表格
| API | 作用 | 特性 |
|---|---|---|
reactive |
深度响应式对象 | 代理原始对象,返回引用类型 |
ref |
基础类型 / 任意值响应式 | 内部通过 value 属性包装 |
computed |
计算属性 | 缓存结果,依赖变更才更新 |
watch |
监听数据变化 | 支持监听 reactive/ref 单个 / 多个值 |
readonly |
只读响应式 | 不可修改,防止意外变更 |
四、核心原理:三大基石
Vue 3 响应式的本质是 「数据劫持 + 依赖收集 + 依赖触发」,三大核心模块协同工作:
1. 数据劫持:Proxy 代理
Proxy 是 ES6 提供的代理对象,可拦截对象 / 数组 的13 种操作(取值、赋值、删除、遍历等),Vue 3 用它做一层「拦截壳」:
javascript
// 极简代理示例
const target = { name: "Vue3" };
const proxy = new Proxy(target, {
// 拦截取值:get
get(target, key) {
const res = Reflect.get(target, key);
// TODO: 收集依赖
return res;
},
// 拦截赋值:set
set(target, key, value) {
const res = Reflect.set(target, key, value);
// TODO: 触发依赖
return res;
},
// 拦截删除
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key);
// TODO: 触发依赖
return res;
}
});
✅ 关键点:
Proxy是代理整个对象,而非单个属性,天然支持新增 / 删除属性- 配合
Reflect保证this指向正确,操作结果返回布尔值 - 支持惰性递归:只有访问到嵌套对象时才会代理,性能大幅提升
2. 依赖收集:谁用了数据?
当渲染函数、computed、watch 使用响应式数据时,会触发 get 拦截,此时记录「当前使用数据的函数」,这个过程叫依赖收集。
依赖收集结构(Vue 源码核心存储)
采用 WeakMap → Map → Set 三层结构:
vbnet
WeakMap(targetMap):
key: 原始对象(target)
value: Map(depsMap)
key: 对象属性(key)
value: Set 依赖函数集合(effect)
WeakMap:弱引用,不影响垃圾回收,避免内存泄漏Set:自动去重,避免重复收集依赖
3. 依赖触发:数据变了,通知谁?
当修改响应式数据时,触发 set 拦截,遍历该数据对应的所有依赖函数 并执行,这个过程叫依赖触发,最终实现视图更新。
五、手写简易版 Vue 3 响应式(核心逻辑)
我们用 50 行代码实现 reactive + effect,还原核心原理:
1. 依赖收集与触发工具
ini
// 1. 依赖存储容器
const targetMap = new WeakMap();
// 2. 当前激活的副作用函数(谁在用数据)
let activeEffect = null;
// 收集依赖
function track(target, key) {
if (!activeEffect) return;
// 1. 获取对象对应的依赖Map
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
// 2. 获取属性对应的依赖Set
let dep = depsMap.get(key);
if (!dep) depsMap.set(key, (dep = new Set()));
// 3. 收集当前副作用
dep.add(activeEffect);
}
// 触发依赖
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
dep && dep.forEach(effect => effect());
}
2. 实现 reactive
javascript
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key); // 取值 → 收集依赖
// 惰性递归:若值是对象,继续代理
if (typeof res === "object" && res !== null) {
return reactive(res);
}
return res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
trigger(target, key); // 赋值 → 触发依赖
return res;
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key);
trigger(target, key); // 删除 → 触发依赖
return res;
}
});
}
3. 实现 effect(副作用函数)
ini
function effect(fn) {
activeEffect = fn;
fn(); // 执行函数 → 触发get → 收集依赖
activeEffect = null;
}
4. 测试验证
ini
// 定义响应式对象
const state = reactive({ name: "Vue3", count: 0 });
// 定义副作用(模拟视图渲染)
effect(() => {
console.log("视图更新:", state.name, state.count);
});
// 修改数据 → 自动触发视图更新
state.name = "Vue3 Responsive"; // 视图更新
state.count++; // 视图更新
state.age = 18; // 新增属性 → 依然触发!
六、ref 原理(基础类型响应式)
reactive 只能处理引用类型,ref 用于基础类型(string/number/boolean) 或任意值,原理很简单:
- 内部包装成一个对象,包含
value属性 - 对
value做get/set拦截,同样走「依赖收集 / 触发」
csharp
// 极简 ref 实现
function ref(value) {
return {
_isRef: true,
get value() {
track(this, "value");
return value;
},
set value(newVal) {
value = newVal;
trigger(this, "value");
}
};
}
七、Vue 3 响应式优势总结
- 更强兼容性:支持对象、数组、Set、Map、WeakMap 等
- 更少缺陷:天然支持新增 / 删除属性、数组下标修改
- 更高性能:惰性递归,非深度遍历,弱引用防内存泄漏
- 更灵活:组合式 API 配合响应式,逻辑复用更优雅
八、总结
Vue 3 响应式的核心逻辑可以浓缩为三句话:
- 用 Proxy 劫持数据的所有操作
- 用 track 在 get 时收集谁用了数据
- 用 trigger 在 set 时通知使用者更新
理解这套原理,不仅能吃透 Vue 3 响应式 API,更能轻松排查响应式丢失、视图不更新等常见问题,是 Vue 进阶必备核心知识。