本篇关键词:响应式本质、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 的答案非常优雅:
| 概念 | 对应源码 |
|---|---|
| 依赖收集 | track() |
| 触发更新 | trigger() |
| 副作用函数 | effect() |
四、手写 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 源码 |
|---|---|
activeEffect |
effectStack |
track() |
track() |
trigger() |
trigger() |
deps |
Dep类型 |
👉 区别只是:源码多了调度器、递归代理、边界处理。
六、工程视角:为什么这样设计?(VIP 重点)
1️⃣ 为什么用 WeakMap?
WeakMap → key 必须是对象
好处:
-
对象销毁 → 键值自动回收
-
避免手动清理依赖(Vue2 的痛点)
2️⃣ 为什么 effect 要支持嵌套?
effect(() => {
effect(() => {
console.log(state.count)
})
})
Vue3 使用 effectStack 解决:
effectStack.push(effect)
activeEffect = effect
3️⃣ 为什么需要调度器(Scheduler)?
试想:
state.count++
state.count++
你不希望触发两次更新。
Vue3 通过:
-
queueJob -
nextTick -
flushPostFlushCbs
实现异步批量更新。
(下一篇详细讲)
七、调试实战:在源码中验证你的理解
回到第二篇搭建的环境,在以下位置打断点:
packages/reactivity/src/effect.ts
重点关注:
-
track()何时被调用 -
trigger()如何遍历dep -
activeEffect的变化过程
你会发现:
👉 源码逻辑和你手写的 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 源码解析,搞懂响应式对象创建过程》