本篇关键词:响应式本质、Proxy、依赖收集、effect、track、trigger
难度等级:⭐⭐⭐⭐☆(偏底层,但全程带 Demo)
工程价值:读懂这一篇,你能手写简易状态管理库、精准定位响应式 Bug、面试秒杀 90% 候选人。
一、开篇:为什么 Vue3 要抛弃 Object.defineProperty?
如果你用过 Vue2,一定写过这样的代码:
data() { return { list: [1, 2, 3] } }然后你发现:
this.list[0] = 99 // ❌ 不触发更新 this.list.length = 0 // ❌ 不触发更新 delete this.obj.a // ❌ 不触发更新Vue2 的解决方案是:set、delete、重写数组方法。
这些不是“特性”,而是补丁。
Vue2 响应式的本质缺陷
Object.defineProperty只能做到:
✅ 劫持已有属性
❌ 无法监听新增 / 删除属性
❌ 无法监听数组索引 / length
❌ 需要递归遍历对象,性能差
👉结论:Vue2 的响应式是“后天修补出来的”。
二、Vue3 的解法:Proxy 带来的降维打击
Vue3 使用Proxy,一句话概括其优势:
Proxy 是对整个对象的拦截,而不是对属性的拦截。
Proxy 最直观的 Demo(先感性理解)
const obj = { count: 0 } const proxy = new Proxy(obj, { get(target, key, receiver) { console.log(`读取 ${key}`) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log(`修改 ${key} = ${value}`) return Reflect.set(target, key, value, receiver) } }) proxy.count // 读取 count proxy.count = 1 // 修改 count = 1✅ 新增属性?能拦截
✅ 删除属性?能拦截
✅ 数组索引?能拦截
✅ 嵌套对象?懒代理(后面讲)
三、响应式系统的灵魂三问(源码级思维)
在阅读 Vue3 响应式源码前,必须先回答三个问题:
谁用了这个数据?(依赖收集)
数据变了通知谁?(依赖触发)
如何避免无限循环?(调度控制)
Vue3 的答案非常优雅:
概念 | 对应源码 |
|---|---|
依赖收集 |
|
触发更新 |
|
副作用函数 |
|
四、手写 Mini Reactive(VIP 核心环节)
不先写 Demo,直接读源码 = 劝退。
下面我们用不到 30 行代码,实现一个最简响应式系统。
Step 1:存储依赖的数据结构
// 存储所有副作用 const targetMap = new WeakMap() // 当前正在执行的 effect let activeEffect = null设计说明:
WeakMap<target, Map<key, Set<effect>>>为什么用
WeakMap?防止内存泄漏为什么用
Set?防止重复收集
Step 2:track —— 依赖收集
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) }一句话解释:
把“当前正在执行的副作用函数”,存到“目标对象的指定属性”下。
Step 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()) } }一句话解释:
找到“目标对象指定属性的所有副作用”,挨个执行。
Step 4:effect —— 副作用容器
function effect(fn) { activeEffect = fn fn() // 执行一次,触发 track activeEffect = null }关键点:
执行
fn()时,内部读取响应式数据触发
get→ 调用track完成依赖收集
Step 5:reactive —— 创建响应式对象
function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { const result = Reflect.get(target, key, receiver) track(target, key) return result }, set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver) trigger(target, key) return result } }) }Step 6:完整 Demo 跑起来 🎉
const state = reactive({ count: 0 }) effect(() => { console.log('count =', state.count) }) state.count++ // 控制台自动打印:count = 1 state.count = 5 // 控制台自动打印:count = 5✅恭喜你,你已经理解了 Vue3 响应式的 80% 核心逻辑。
五、对照 Vue3 源码:你写的就是简化版源码
你现在写的代码,本质上就是:
packages/reactivity/src/ ├── effect.ts ← effect / track / trigger ├── reactive.ts ← reactive() └── baseHandlers.ts ← Proxy handlers源码中的关键对应关系
手写 | Vue3 源码 |
|---|---|
|
|
|
|
|
|
|
|
👉区别只是:源码多了调度器、递归代理、边界处理。
六、工程视角:为什么这样设计?(VIP 重点)
1️⃣ 为什么用 WeakMap?
WeakMap → key 必须是对象好处:
对象销毁 → 键值自动回收
避免手动清理依赖(Vue2 的痛点)
2️⃣ 为什么 effect 要支持嵌套?
effect(() => { effect(() => { console.log(state.count) }) })Vue3 使用effectStack 解决:
effectStack.push(effect) activeEffect = effect3️⃣ 为什么需要调度器(Scheduler)?
试想:
state.count++ state.count++你不希望触发两次更新。
Vue3 通过:
queueJobnextTickflushPostFlushCbs
实现异步批量更新。
(下一篇详细讲)
七、调试实战:在源码中验证你的理解
回到第二篇搭建的环境,在以下位置打断点:
packages/reactivity/src/effect.ts重点关注:
track()何时被调用trigger()如何遍历depactiveEffect的变化过程
你会发现:
👉源码逻辑和你手写的 Mini Reactive 几乎一致。
八、面试高频追问(收藏级)
Proxy 一定能监听到所有变化吗?
不能。
Map、Set需要单独处理(mutableHandlersvscollectionHandlers)Vue3 为什么还要 ref?
Proxy 无法代理基本类型,
ref本质是{ value }对象的语法糖。track 和 trigger 一定是同步的吗?
不一定。计算属性、watch、组件更新都可能被调度延迟。
九、本篇总结
✅ Vue3 响应式 = Proxy + WeakMap + Effect
✅ 核心流程:读取收集 → 修改触发
✅ 手写 Mini Reactive 是读源码的最短路径
✅ 工程中理解调度器、嵌套 effect、内存管理
一句话升华:
Vue3 的响应式不是“魔法”,而是一个设计极其精巧的观察者模式 + 代理模式组合。
十、下一篇预告
👉《Vue3 源码详解(四):reactive 与 ref 源码解析,搞懂响应式对象创建过程》