DRAFT Vue3 响应式原理
非响应式
一般的 JavaScript 对象不会追踪数据更新。当它被改变时,依赖它的副作用函数完全不知情。
const obj = {
name: "张三",
age: 18,
};
function changeAge() {
document.body.innerHTML = obj.age;
}
changeAge();
setTimeout(() => {
obj.age = 19; // 1 秒后 age 改变,但视图不会更新
}, 1000);上面的例子中,虽然 obj.age 被改变了,但函数 changeAge 并不知道它改变,因此不会重新执行,视图就无法更新。如果我们能够拦截一个对象属性的读取和修改,就能在数据变化时主动通知依赖方,实现响应式。
Vue 2 的响应式
Object.defineProperty 拦截
Vue 2 的响应式核心是 Object.defineProperty。它允许我们为对象的每个属性定义 getter 和 setter,从而拦截对该属性的读取与赋值。
// 定义一个函数,将普通对象转换为响应式对象
function defineReactive(obj, key) {
let value = obj[key];
Object.defineProperty(obj, key, {
// 拦截读取操作
get() {
console.log(`读取了 ${key}`);
return value;
},
// 拦截赋值操作
set(newValue) {
console.log(`设置了 ${key} = ${newValue}`);
value = newValue;
},
});
}
const student = { name: "张三" };
// 将 name 属性变为响应式
defineReactive(student, "name");
student.name; // 控制台打印:读取了 name
student.name = "李四"; // 控制台打印:设置了 name = 李四在这个基础上,Vue 2 在 setter 中触发视图更新,在 getter 中收集依赖(即 Watcher),就实现了一个基本的响应式系统。
Vue 2 响应式的不足
Vue 2 的方式存在几个难以规避的问题:
- 无法检测属性的添加和删除。 Vue 2 在初始化时会对
data中已声明的属性逐一调用Object.defineProperty,把这些属性转为 getter/setter。因此之后动态添加的新属性不会被劫持,需要用Vue.set/Vue.delete来补这个能力。
const state = { name: "张三" };
// 将 state 变为响应式后,新增 age 属性:
state.age = 18; // Vue 2 无法检测到这次变更无法直接监听数组索引和 length 变化。 通过索引修改数组元素(
arr[0] = 'x')或直接修改length,Object.defineProperty无法拦截。Vue 2 通过重写数组的 7 个变异方法(push、pop、shift等)来绕过这一限制,但直接索引赋值仍然无法检测。初始化时需要递归遍历所有属性。 对象层级越深,初始化开销越大。即使某些深层属性从未被访问,也会在初始化时被劫持。
const hugeData = {
level1: {
level2: {
level3: {
// ... 很多深层数据
}
}
}
};
// Vue 2 在初始化时会递归遍历所有嵌套属性,逐一调用 Object.defineProperty这些不足的根本原因在于:Object.defineProperty 是对属性的操作,而非对对象的操作。它无法感知对象结构的变化(增删属性)和数组索引操作。
[!QUESTION] 为什么 Vue 2 没有修复这些问题? 这些不足的根源在于:
Object.defineProperty只能对单个属性做劫持,而且必须在初始化时就逐个属性调用。它本身无法感知对象结构的变化(增删属性)和数组索引操作,因此用同一套 API 很难彻底解决这些问题。Vue 3 改用Proxy,正是为了突破 ES5 的能力上限。
Vue 3 的响应式
Vue 3 使用 Proxy 替代 Object.defineProperty 作为响应式的核心机制。
Proxy 是 ES6 新增的 API。它可以创建一个对象的代理,拦截对该对象的任何操作——不仅是属性读写,还包括属性删除、in 操作符、for...in 遍历等,共 13 种拦截行为。
与 Object.defineProperty 的根本区别在于:Proxy 代理的是整个对象,而非单个属性。这意味着新增属性、删除属性、数组索引操作都能被拦截。
创建 Proxy 对象
const obj = {
name: "张三",
age: 18,
};
const proxyData = new Proxy(obj, {
// 拦截属性读取
// target:被代理的原始对象(即 obj)
// key:被读取的属性名(如 'name'、'age')
get(target, key) {
console.log(`读取了 ${String(key)}`);
return target[key];
},
// 拦截属性赋值
// target:被代理的原始对象(即 obj)
// key:被设置的属性名
// value:被设置的新值
set(target, key, value) {
console.log(`设置了 ${String(key)} = ${value}`);
target[key] = value;
return true; // 必须返回 true,表示设置成功
},
});
proxyData.name; // 读取了 name
proxyData.age = 20; // 设置了 age = 20
proxyData.gender = "男"; // 设置了 gender = 男 —— 新增属性也能拦截!这样,新增 gender 属性也能被 set 拦截,这是 Object.defineProperty 做不到的。
添加副作用函数
当数据变化时,需要在 set 中执行相应的副作用函数(更新视图)。我们用 Set 暂存副作用函数,在 get 中收集、在 set 中触发。
// 原始数据
const obj = {
name: "Tom",
age: 18,
};
// 副作用函数 —— 更新页面的函数
function effect() {
document.body.innerText = proxyData.name;
}
// 使用 Set 保存副作用函数
const effectFns = new Set();
const proxyData = new Proxy(obj, {
get(target, key) {
// 读取属性时,将副作用函数收集到 Set 中
effectFns.add(effect);
return target[key];
},
set(target, key, value) {
target[key] = value;
// 赋值后,遍历执行所有副作用函数
effectFns.forEach((fn) => fn());
return true;
},
});
// 首次执行副作用函数,触发 get 完成依赖收集
effect();
// 1 秒后修改数据,自动触发 set 中的副作用执行
setTimeout(() => {
proxyData.name = "Bob";
}, 1000);这个例子展示了响应式最核心的工作原理:读取时收集依赖,写入时触发更新。
但这里有一个问题:副作用函数 effect 是硬编码的。真实场景中,副作用函数可能是匿名函数,也可能有多个。我们需要一个通用的注册机制。
注册副作用函数
Vue 3 中定义了一个 effect 函数来统一注册副作用函数。它用一个全局变量 activeEffect 来标记 " 当前正在执行的副作用函数 ",这样 Proxy 的 get 中就能知道该收集谁。
// 全局变量,存储当前正在执行的副作用函数
let activeEffect = null;
// effect 函数:注册并立即执行副作用函数
function effect(fn) {
// 包装一层,在 fn 执行前将 activeEffect 指向自己
const effectFn = () => {
activeEffect = effectFn;
fn(); // 执行 fn 时访问响应式数据,触发 get,get 中将 activeEffect 收集为依赖
};
// 立即执行一次,完成初始的依赖收集
effectFn();
}使用时只需将副作用函数传入 effect:
effect(() => {
document.body.innerText = proxyData.name;
});effect 执行后,activeEffect 被设置为包装后的 effectFn,然后执行传入的 fn。fn 中访问了 proxyData.name,触发 get,此时 get 中拿到的 activeEffect 正是这个 effectFn,于是将它收集起来。
以下是按照你要求的“层层递进”顺序优化后的三个章节。内容在原文基础上调整了结构,将 track 和 trigger 分别在依赖收集和触发更新章节末尾封装好,再在深层响应式章节自然地使用它们,同时补全了 Reflect 和 receiver 的说明。
依赖收集
上面的例子用单个 Set 存储所有副作用函数,这会导致一个问题:修改 name 时,依赖 age 的副作用函数也会执行,因为此时 effectFns 记录的副作用函数并没有记录它依赖于哪一个属性。
为此,需要建立 响应式对象 → 属性 → 副作用函数 的映射关系。Vue 3 使用如下数据结构:
WeakMap<target, Map<key, Set<effectFn>>>| 层级 | 类型 | 说明 |
|---|---|---|
| 第一层 | WeakMap | key 是响应式对象(target),value 是 depsMap |
| 第二层 | Map | key 是属性名,value 是 deps |
| 第三层 | Set | 存储所有依赖该属性的副作用函数 |
[!NOTE] 为什么外层用 WeakMap?
WeakMap的 key 是弱引用,不会阻止垃圾回收。当响应式对象在外部没有其他引用时,它能被自动回收,避免内存泄漏。如果用Map,被代理的对象即使已经没有用处了,也仍然被 Map 引用着,无法被 GC 回收。
将上述结构融入 Proxy,得到:
// 原始数据
const data = { text: "hello world" };
// 存储副作用函数的"桶" —— 即 WeakMap
const bucket = new WeakMap();
// 当前激活的副作用函数
let activeEffect = null;
// effect 函数:注册副作用函数
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn;
fn();
};
effectFn();
}
// 对原始数据的代理
const obj = new Proxy(data, {
get(target, key) {
// 如果没有正在执行的副作用函数,直接返回
if (!activeEffect) return target[key];
// 从 bucket 取出所属 target 的 depsMap
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 从 depsMap 取出所属 key 的 deps(Set)
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 将当前激活的副作用函数添加到依赖集合
deps.add(activeEffect);
// 反向记录:让 effectFn 知道自己被哪些 deps 引用,便于后续清理,在后续依赖章节说明
activeEffect.deps.push(deps);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
// 取出 target 对应的 depsMap
const depsMap = bucket.get(target);
if (!depsMap) return true;
// 取出 key 对应的所有副作用函数,逐一执行
const effects = depsMap.get(key);
if (effects) {
effects.forEach((fn) => fn());
}
return true;
},
});这段代码里,get 拦截器中的依赖收集逻辑可以封装成一个独立的 track 函数,让后续的代码复用:
function track(target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
// 反向记录,用于 cleanup
activeEffect.deps.push(deps);
}于是 Proxy 的 get 可以简化为:
get(target, key) {
track(target, key); // 收集依赖
return target[key];
}至此,依赖收集的核心逻辑已经被提取为 track 函数,完成属性的副作用函数收集。
依赖清理
考虑下面的场景:
effect(() => {
// ok 为 true 时读取 text,为 false 时只读取 name
document.body.innerText = obj.ok ? obj.text : obj.name;
});当 obj.ok 为 true 时,副作用函数依赖 ok 和 text。当 obj.ok 变为 false 后,text 不再被读取,但它的依赖关系仍然保留着。此后修改 obj.text 时,这个副作用函数仍会被执行 —— 而此时 text 的值根本用不到。这就是 " 过期 " 依赖:曾经被依赖、但现在已不相关的副作用关系。
这些过期依赖不仅浪费性能,长期累积还可能导致内存问题。解决方法是在副作用函数每次执行前,先清空它之前的所有依赖关系,然后在执行过程中重新建立依赖。
// 清理函数:将 effectFn 从它关联的所有 deps 中移除
function cleanup(effectFn) {
// effectFn.deps 是一个数组,存储了所有包含该 effectFn 的 Set
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn);
}
// 清空记录
effectFn.deps.length = 0;
}修改 effect 函数,在执行 fn 之前先调用清理,并为 effectFn 挂载 deps 数组:
function effect(fn) {
const effectFn = () => {
// 每次执行前,先清除已有的所有依赖关联
cleanup(effectFn);
activeEffect = effectFn;
fn();
};
// 挂载 deps 数组,用于记录该 effectFn 被哪些 Set 所包含
effectFn.deps = [];
effectFn();
}由于 JavaScript 中函数也是对象,所以我们可以直接在
effectFn上添加自定义属性deps。
这样一来,track 函数中 activeEffect.deps.push(deps) 就会顺利地将依赖集合记录到 effectFn.deps 数组中。每次重新执行时,旧的依赖被清除,新的依赖重新建立,保证依赖集合始终是最新的。
触发更新
“触发更新”是指数据变更后,如何一步步通知到所有依赖方并最终反映到视图上。完整流程如下:
数据变更(set)
→ Proxy 的 set 拦截器
→ trigger(target, key)
→ 从 bucket 取出 depsMap
→ 从 depsMap 取出 key 对应的 effects Set
→ 遍历 effects,逐一执行 effectFn
→ 每个 effectFn 重新执行传入的 fn
→ fn 中更新 DOM / 执行其他副作用这个流程的核心是 trigger 函数,我们可以将之前 set 中的逻辑封装为:
// 简化的 trigger 函数
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
if (!effects) return;
// 创建副本再遍历,避免在执行过程中 effects 被修改导致无限循环
const effectsToRun = new Set(effects);
effectsToRun.forEach((effectFn) => {
// 避免在 effect 中修改自身依赖数据导致无限递归
if (effectFn !== activeEffect) {
effectFn();
}
});
}有了 trigger,Proxy 的 set 就可以写成:
set(target, key, value) {
target[key] = value;
trigger(target, key); // 触发依赖更新
return true;
}trigger 在 Vue 3 源码中的职责更复杂,包括:
- 取出目标 key 的所有副作用函数
- 支持数组
length变更相关的额外触发逻辑 - 支持
for...in遍历相关的ITERATE_KEY依赖触发 - 根据调度器(scheduler)决定副作用函数的执行时机,实现
nextTick批处理等异步更新策略
深层响应式
有了 track 和 trigger,我们就可以进一步实现能代理嵌套对象的 reactive 函数。
在此之前,需要先了解 Reflect。Reflect 是 ES6 提供的内置对象,它拥有一系列与 Proxy 拦截器一一对应的静态方法(如 Reflect.get、Reflect.set、Reflect.deleteProperty 等)。与直接使用 target[key] 相比,使用 Reflect 方法的核心优势在于:可以传递 receiver。receiver 是 get/set 拦截器的第三个参数,它指向发起当前操作的代理对象 ,从而保证对象内部的 this 指向代理对象,而不是原始对象**。
考虑一个场景:原始对象内部有 getter,且 getter 中访问了 this 上的属性。
const raw = {
firstName: '张',
lastName: '三',
get fullName() {
return this.firstName + this.lastName;
}
};
const state = reactive(raw);当读取 state.fullName 时,fullName 的 getter 会执行,并访问 this.firstName 和 this.lastName。如果我们在 get 拦截器里写 return target[key],this 将指向原始对象 raw,后续对 firstName 的读取不会触发代理的 get 拦截,依赖无法被收集。而使用 Reflect.get(target, key, receiver) 会将 receiver(即代理对象 state)作为 getter 的 this 传入,确保 this.firstName 再次经过代理,依赖收集正常运作。set 同理。
明白了这些之后,来看 reactive 的完整实现:
// 判断一个值是否为对象(排除 null)
function isObject(val) {
return val !== null && typeof val === "object";
}
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 收集依赖
track(target, key);
// 使用 Reflect 并传入 receiver,保证 getter 中的 this 正确
const result = Reflect.get(target, key, receiver);
// 如果读取的结果是对象,递归创建深层响应式
if (isObject(result)) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
// 使用 Reflect.set 并传入 receiver,保证 setter 中的 this 正确
const result = Reflect.set(target, key, value, receiver);
// 值真的发生变化时才触发更新(用 Object.is 能正确处理 NaN)
if (!Object.is(oldValue, value)) {
trigger(target, key); // 触发依赖(已封装好的 trigger)
}
return result;
},
});
}这样,访问 state.nested.count 时,每一层的对象都会被自动包装为响应式代理,深层属性的变更也能被检测到。整个响应式系统的核心骨架至此搭建完成。
基本类型的响应式
Proxy 只能代理对象(引用类型),无法直接代理基本类型(string、number、boolean 等),因为基本类型是按值传递的,本身没有属性可供拦截。
要让基本类型变成响应式,需要将它包装成一个对象:
// ref 的实现思路:将基本类型装入对象
function ref(rawValue) {
// 创建一个包含 value 属性的普通对象
const wrapper = {
value: rawValue,
};
// 将这个对象变为响应式
return reactive(wrapper);
}当访问 refValue.value 时,实际是在访问响应式对象的 value 属性,get 拦截器就能正常工作;修改 refValue.value 时,set 拦截器也能触发更新。
Vue 3 中 ref 的实际实现没有直接使用 reactive,底层通过 .value 的 getter/setter 实现,内部遇到对象会复用 reactive。
function ref(rawValue) {
// 如果传入的是对象,内部用 reactive 处理
const innerValue = isObject(rawValue) ? reactive(rawValue) : rawValue;
const r = {
get value() {
track(r, "value");
return innerValue;
},
set value(newValue) {
if (rawValue !== newValue) {
rawValue = newValue;
innerValue = isObject(newValue) ? reactive(newValue) : newValue;
trigger(r, "value");
}
},
};
return r;
}也就是说,ref(对象) 在底层等价于 reactive(对象),只不过多了一层 .value 的包装。
Reactive vs Ref
区别
| 对比维度 | reactive | ref |
|---|---|---|
| 适用类型 | 对象、数组、Map、Set 等引用类型 | 任意类型(基本类型 + 引用类型) |
| 访问方式 | 直接访问属性:state.count | 通过 .value:count.value |
| 模板中使用 | 直接使用:{{ state.count }} | 自动解包:{{ count }}(无需 .value) |
| 解构后 | 失去响应式 | 保持响应式(每个 ref 是独立的) |
| 替换整个值 | Object.assign 逐属性赋值 | 直接赋值 ref.value = newVal |
| 传递方式 | 传递对象引用 | 传递 ref 对象本身(.value 可独立替换) |
关键差异举例:
// reactive:解构会丢失响应式
const state = reactive({ count: 0, name: "vue" });
const { count, name } = state; // count 和 name 已经不再是响应式的
// ref:每个 ref 是独立响应式单元,可以单独传递
const count = ref(0);
const name = ref("vue");
const { value: c } = count; // c 不是响应式,但 count 仍是// reactive:不能整体替换
let state = reactive({ count: 0 });
state = reactive({ count: 1 }); // 创建了新对象,旧的依赖关系断裂
// ref:可以整体替换 .value
const count = ref(0);
count.value = 1; // 仍然是同一个 ref 对象,依赖关系不变差异的原因
reactive的响应式建立在对象本身。它是通过
Proxy直接代理目标对象,拦截的是对象属性的操作。因此必须始终操作同一个代理对象,解构或替换都意味着脱离了这个 Proxy。const raw = { count: 0 }; const state = reactive(raw); // state 是 raw 的 Proxy 包装 // state.count → 触发 Proxy 的 get // 解构后:const { count } = state → count 只是一个普通数字 0,与 Proxy 无关ref的响应式建立在包装器对象上。ref创建的是一个带getter/setter的包装器对象,.value是唯一的数据出入口。const count = ref(0); // count 本身是 { get value() {...}, set value(val) {...} } // count.value → 触发 getter → track(count, 'value') // count.value = 1 → 触发 setter → trigger(count, 'value')由于响应式附着在 ref 对象本身 而非内部值上,传递 ref 对象就意味着传递响应式能力,内部值可以随意替换。
模板中 ref 的自动解包。
模板编译时,Vue 会检测到
ref对象,自动为其追加.value。reactive对象则直接访问其属性,不存在解包步骤。模板:{{ count }} 编译后大致等价于:{{ count.value }}
[!SUMMARY] 区别
reactive:Proxy 代理整个对象,响应式绑定在对象本体上,不能解构、不能整体替换ref:包装器对象持有.value入口,响应式绑定在包装器上,可以自由替换.value,也正因如此才需要.value来访问
实现一个简单的响应式系统
下面实现一个包含 reactive、ref、effect 的简易响应式系统,代码可直接运行。
// ============================================================
// 1. 工具函数
// ============================================================
// 判断是否为对象(排除 null)
function isObject(val) {
return val !== null && typeof val === "object";
}
// ============================================================
// 2. 依赖存储核心
// ============================================================
// 存储所有依赖关系的"桶":WeakMap<target, Map<key, Set<effectFn>>>
const targetMap = new WeakMap();
// 当前正在执行的副作用函数
let activeEffect = null;
// ============================================================
// 3. track:在 get 中收集依赖
// ============================================================
function track(target, key) {
// 没有 activeEffect 说明当前不在 effect 上下文中,无需收集
if (!activeEffect) return;
// 从 targetMap 取出 target 的 depsMap
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 从 depsMap 取出 key 对应的 deps(副作用函数集合)
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 将当前副作用函数加入依赖集合
deps.add(activeEffect);
// 反向记录:方便 cleanup 时从 deps 中删除自己
activeEffect.deps.push(deps);
}
// ============================================================
// 4. trigger:在 set 中触发更新
// ============================================================
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (!deps) return;
// 创建副本再遍历:防止在遍历过程中 deps 被修改(cleanup 会在
// 执行 effectFn 前清除 deps 中原有的 effectFn,再重新添加)
const effectsToRun = new Set(deps);
effectsToRun.forEach((effectFn) => {
// 防止在 effect 中修改自身依赖的数据导致无限递归
if (effectFn !== activeEffect) {
effectFn();
}
});
}
// ============================================================
// 5. cleanup:清理过期依赖
// ============================================================
function cleanup(effectFn) {
// effectFn.deps 中存储了所有包含该 effectFn 的 Set
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
// 从每个 Set 中移除自身
deps.delete(effectFn);
}
// 清空记录
effectFn.deps.length = 0;
}
// ============================================================
// 6. effect:注册副作用函数
// ============================================================
function effect(fn) {
const effectFn = () => {
// 每次执行前,先清除之前建立的依赖关系
cleanup(effectFn);
// 将自身设为当前活跃的副作用函数
activeEffect = effectFn;
// 执行传入的函数,执行过程中访问响应式数据 → track → 建立新依赖
fn();
// 执行完毕后置空
activeEffect = null;
};
// 挂载 deps 数组,存储所有包含此 effectFn 的依赖集合
effectFn.deps = [];
// 立即执行,完成首次依赖收集
effectFn();
}
// ============================================================
// 7. reactive:创建深层响应式对象
// ============================================================
function reactive(target) {
// 如果不是对象,直接返回(reactive 只能代理对象)
if (!isObject(target)) {
console.warn("reactive 只能接收对象类型");
return target;
}
return new Proxy(target, {
get(target, key, receiver) {
// 收集依赖
track(target, key);
// 通过 Reflect 获取值(保证 this 指向正确)
const result = Reflect.get(target, key, receiver);
// 深层响应式:如果值是对象,递归包装
if (isObject(result)) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
// 通过 Reflect 设置值
const result = Reflect.set(target, key, value, receiver);
// 值确实发生变化时才触发更新
if (oldValue !== value) {
trigger(target, key);
}
return result;
},
// 拦截 delete 操作(Object.defineProperty 无法拦截这个)
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;
},
});
}
// ============================================================
// 8. ref:创建基本类型 / 引用类型的响应式包装
// ============================================================
function ref(rawValue) {
// 如果传入的是对象,内部使用 reactive 处理
if (isObject(rawValue)) {
rawValue = reactive(rawValue);
}
const r = {
get value() {
track(r, "value");
return rawValue;
},
set value(newValue) {
if (newValue !== rawValue) {
rawValue = isObject(newValue) ? reactive(newValue) : newValue;
trigger(r, "value");
}
},
};
return r;
}
// ============================================================
// 9. 验证 —— 打开浏览器控制台或 Node.js 环境执行以下代码
// ============================================================
// ---- 测试 reactive ----
const state = reactive({
count: 0,
nested: { text: "hello" },
});
effect(() => {
console.log("count 更新为:", state.count);
});
effect(() => {
console.log("nested.text 更新为:", state.nested.text);
});
state.count = 1;
// 控制台输出:count 更新为: 1
state.nested.text = "world";
// 控制台输出:nested.text 更新为: world
// ---- 测试 ref ----
const num = ref(0);
effect(() => {
console.log("num 更新为:", num.value);
});
num.value = 10;
// 控制台输出:num 更新为: 10
// ---- 测试依赖清理 ----
const obj = reactive({ ok: true, text: "hello" });
effect(() => {
console.log(obj.ok ? obj.text : "不存在");
});
// 控制台输出:hello
obj.ok = false;
// 控制台输出:不存在
// 此后修改 obj.text 不会再触发该副作用(因为已不再依赖 text)
obj.text = "world";
// 控制台无输出 ✅这个实现覆盖了响应式系统的核心机制:依赖收集(track)、派发更新(trigger)、依赖清理(cleanup)、深层代理(reactive 递归)、基本类型包装(ref)。它与 Vue 3 真实源码的主要差异在于:缺少调度器(scheduler)、computed/watch 等衍生 API、readonly/shallowReactive 等变体,但作为理解响应式原理的起点已经足够。
学习参考资料:Vue 3 深入响应式系统