要搞懂Vue3的响应式原理,咱们先抛开专业术语,用奶茶店接单的生活化例子打底,再拆解核心逻辑,最后用简单代码模拟,保证一看就懂。
核心目标:数据变,页面/逻辑自动更
Vue3响应式的本质就是:当你修改数据时,所有用到这个数据的地方(比如页面显示、计算属性、watch)会自动更新。
就像奶茶店:
- 顾客(数据)点了「珍珠奶茶」(数据值);
- 后厨(页面/计算属性)要按这个要求备料;
- 顾客改要求(改数据)为「芋圆奶茶」,后厨不用你喊,自动换料(自动更新)。
第一步:给数据装「管家」------ Proxy代理
Vue3之所以能"感知"数据变化,核心是给数据套了一层「代理(Proxy)」,这个代理就像奶茶店的前台管家:不管是"看数据"(读)还是"改数据"(写),都必须经过它。
对比Vue2(为啥换Proxy?)
Vue2用的是Object.defineProperty,只能监听对象的已有属性,比如:
- 改数组下标(
arr[0] = 1)监听不到; - 给对象加新属性(
obj.newKey = 1)监听不到。
而Proxy是"全方位监听",能覆盖对象/数组的所有操作(新增、删除、改下标都能感知)。
通俗理解Proxy
假设你有个数据:
js
const data = { toppings: '珍珠' }
给它套上Proxy后,就像这样:
js
// 管家(Proxy)
const proxyData = new Proxy(data, {
// 读数据时触发(比如页面渲染用data.toppings)
get(target, key) {
console.log('有人看了', key, '的值');
return target[key]; // 把值返回
},
// 改数据时触发(比如data.toppings = '芋圆')
set(target, key, value) {
console.log('有人改了', key, '的值,新值是', value);
target[key] = value; // 把新值存进去
return true;
}
});
此时你操作proxyData.toppings,管家都会立刻知道:
js
console.log(proxyData.toppings); // 打印:有人看了 toppings 的值 → 珍珠
proxyData.toppings = '芋圆'; // 打印:有人改了 toppings 的值,新值是 芋圆
第二步:记下来「谁用到了数据」------ 依赖收集
管家知道了"谁看/改数据"还不够,得记下来:哪些地方用到了这个数据(比如页面渲染、计算属性),这些"地方"就是「依赖」。
依赖收集的时机
只有当「读数据」时(比如页面首次渲染、计算属性执行),管家才会收集依赖------就像顾客点单时,管家把"顾客A要珍珠奶茶"记在订单本上。
依赖存哪?(简单版容器)
Vue3用三层容器存依赖,咱们简化理解:
targetMap(WeakMap)→ 存「响应式对象」对应的依赖表
└ depsMap(Map)→ 存「对象属性」对应的依赖集合
└ dep(Set)→ 存「用到这个属性的所有副作用函数」
- 副作用函数:凡是用到响应式数据的函数(比如页面渲染函数、
effect、watch回调),执行它会产生"副作用"(比如改变页面)。 - 用Set是为了避免重复:比如同一个数据在页面用了两次,只需要更新一次。
第三步:数据变了,喊「依赖」更新------ 触发更新
当你改数据时(管家的set触发),管家会翻订单本,找到所有用到这个数据的依赖,挨个通知它们"数据变了,快更!"------就像顾客改配料,管家喊后厨:"A和B的订单都换成芋圆!"。
完整流程(奶茶店版)
- 初始化 :给
{ toppings: '珍珠' }套上Proxy管家; - 首次渲染 :页面要显示
{``{ toppings }},读toppings→ 管家触发get→ 把"页面渲染函数"记到toppings的依赖集合里; - 修改数据 :改
toppings = '芋圆'→ 管家触发set→ 找到依赖集合里的"页面渲染函数" → 执行函数 → 页面更新为"芋圆"。
补充:Ref是干啥的?
Proxy只能代理对象/数组 ,没法代理字符串、数字等基本类型(比如let age = 18)。
所以Vue3搞了ref:把基本类型包成「带value属性的对象」,再代理这个对象的value属性。
js
const age = ref(18); // 实际是 { value: 18 }
age.value = 20; // 改value才会触发更新
模板里不用写.value,是因为Vue自动帮你"解包"了(比如<div>{``{ age }}</div>)。
用代码模拟Vue3响应式核心
下面的代码去掉了Vue3的复杂逻辑,只保留核心,能直观看到"收集依赖→触发更新"的过程:
js
// 1. 存储依赖的容器:targetMap → depsMap → dep
const targetMap = new WeakMap();
// 2. 收集依赖(track:追踪)
function track(target, key) {
// 找当前对象的依赖表
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 找当前属性的依赖集合
let dep = depsMap.get(key);
if (!dep) {
dep = new Set(); // Set避免重复依赖
depsMap.set(key, dep);
}
// 把当前的副作用函数加入依赖
dep.add(activeEffect);
}
// 3. 触发依赖(trigger:触发)
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
// 执行所有依赖的副作用函数
dep.forEach(effect => effect());
}
}
// 4. 全局变量:当前正在执行的副作用函数
let activeEffect;
// 5. 注册副作用函数(比如页面渲染、watch)
function effect(fn) {
activeEffect = fn;
fn(); // 执行一次,触发track收集依赖
activeEffect = null;
}
// 6. 模拟reactive:创建响应式对象
function reactive(target) {
return new Proxy(target, {
get(target, key) {
const res = Reflect.get(target, key);
track(target, key); // 读数据时收集依赖
return res;
},
set(target, key, value) {
const res = Reflect.set(target, key, value);
trigger(target, key); // 改数据时触发更新
return res;
}
});
}
// 测试:模拟页面渲染
const data = reactive({ toppings: '珍珠' });
// 注册副作用函数(模拟页面渲染)
effect(() => {
console.log('页面更新:奶茶配料=' + data.toppings);
});
// 改数据,触发更新
data.toppings = '芋圆'; // 打印:页面更新:奶茶配料=芋圆
data.toppings = '椰果'; // 打印:页面更新:奶茶配料=椰果
关键总结
Vue3响应式的核心就3步:
- 代理拦截:用Proxy包裹数据,拦截读(get)和写(set);
- 收集依赖:读数据时(get),把用到数据的副作用函数记下来;
- 触发更新:改数据时(set),执行所有记下来的副作用函数。
额外补充:
reactive:处理对象/数组,默认深响应(嵌套对象也能监听);ref:处理基本类型,本质是代理{ value: 原值 };shallowReactive/shallowRef:浅响应,只监听第一层属性;WeakMap:依赖容器用它是为了内存友好(数据销毁后自动回收依赖,不泄漏)。