前言
前面的文章中,我们已经反复提到:
Vue3 的响应式基于 Proxy + Reflect。
你也知道了:
-
reactive创建响应式对象 -
effect是副作用函数 -
track收集依赖 -
trigger触发更新
但问题是:
这些东西在源码里到底是怎么串起来的?
这一篇,我们从零手写一个 Mini-Vue 响应式系统,只用一个文件、不到 100 行代码,就把 Vue3 响应式的核心链路跑通。
写完之后你会发现:
原来 Vue3 的响应式并没有想象中那么神秘。
一、目标:我们要实现什么?
最终效果:
const state = reactive({ count: 0 })
effect(() => {
console.log('count:', state.count)
})
state.count++ // 控制台自动打印最新值
✅ 数据变化 → 自动重新执行副作用函数
这就是 Vue3 响应式的最小可用模型。
二、核心设计思路
┌─────────────────────────────────────────────┐
│ 响应式系统 │
├─────────────────────────────────────────────┤
│ │
│ reactive(obj) ──→ Proxy 代理对象 │
│ │ │
│ ▼ │
│ 读取属性 (get) ──→ track() 收集依赖 │
│ │ │
│ ▼ │
│ 修改属性 (set) ──→ trigger() 触发更新 │
│ │ │
│ ▼ │
│ effect() 重新执行 │
│ │
└─────────────────────────────────────────────┘
三、数据结构设计
1️⃣ 依赖存储结构
WeakMap (target → Map (key → Set (effects)))
// 存储所有依赖关系
const targetMap = new WeakMap()
为什么用这三层?
| 层级 | 类型 | 作用 |
|---|---|---|
| WeakMap | target → ... | 每个响应式对象一个 Map |
| Map | key → ... | 每个属性一个 Set |
| Set | effects | 该属性的所有副作用函数 |
📌 WeakMap 的好处:
当响应式对象被垃圾回收时,依赖关系自动清除,不会内存泄漏。
四、实现 track(依赖收集)
// 当前正在执行的 effect
let activeEffect = null
function track(target, key) {
if (!activeEffect) return
// 获取 target 对应的 Map
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 获取 key 对应的 Set
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
// 将当前 effect 加入 Set
dep.add(activeEffect)
}
📌 核心逻辑:
把"当前正在运行的副作用函数"和"被读取的属性"建立关联。
五、实现 trigger(触发更新)
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
if (!effects) return
// 依次执行所有副作用函数
effects.forEach(effect => effect())
}
📌 核心逻辑:
找到和这个属性关联的所有副作用函数,全部重新执行。
六、实现 reactive(响应式对象)
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 依赖收集
track(target, key)
// 返回属性值
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
// 设置属性值
const result = Reflect.set(target, key, value, receiver)
// 触发更新
trigger(target, key)
return result
}
})
}
📌 这就是 Vue3 reactive 的最小实现:
-
get 中收集依赖
-
set 中触发更新
-
Reflect 保证正确的 this 指向
七、实现 effect(副作用函数)
function effect(fn) {
// 包装成可调度函数
const effectFn = () => {
activeEffect = effectFn
fn()
activeEffect = null
}
// 立即执行一次
effectFn()
return effectFn
}
📌 关键点:
-
把当前 effect 设为
activeEffect -
执行用户传入的函数(此时会触发 get → track)
-
执行完毕后清空
activeEffect
八、完整代码(不到 60 行)
// ===== Mini-Vue 响应式核心 =====
const targetMap = new WeakMap()
let activeEffect = null
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects?.forEach(fn => fn())
}
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key)
return result
}
})
}
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn
fn()
activeEffect = null
}
effectFn()
return effectFn
}
九、验证:跑一下试试
const state = reactive({ count: 0, name: 'Vue3' })
effect(() => {
console.log(`count is: ${state.count}`)
})
// 输出: count is: 0
state.count++
// 输出: count is: 1
state.count = 10
// 输出: count is: 10
✅ 完美运行!
十、对照 Vue3 源码
| 我们的实现 | Vue3 源码 |
|---|---|
track() |
packages/reactivity/src/effect.ts中的 track |
trigger() |
同文件中的 trigger |
reactive() |
packages/reactivity/src/reactive.ts |
effect() |
packages/reactivity/src/effect.ts |
📌 Vue3 源码更复杂 (处理了数组、Map/Set、嵌套对象等),但核心思想和我们写的一模一样。
十一、局限性说明(诚实交代)
我们的 Mini-Vue 还不能处理:
| 场景 | 说明 |
|---|---|
| 嵌套对象 | 需要递归代理(Vue3 用 Reflect.get+ 懒代理) |
| 数组 | 需要特殊处理 length和索引 |
| Map / Set | 需要重写方法 |
| 多次触发 | 需要 effect 栈防止嵌套 |
| 调度执行 | 需要 scheduler 控制执行时机 |
这些会在后续文章中逐步完善。
十二、面试高频问答
Q1:手写 Vue3 响应式的核心步骤?
用 WeakMap 存储依赖关系
Proxy get 中 track 收集依赖
Proxy set 中 trigger 触发更新
effect 包装函数并立即执行
Q2:为什么用 WeakMap 而不是 Map?
WeakMap 的键是弱引用,不影响垃圾回收,防止内存泄漏。
Q3:Reflect 在这里的作用?
保证正确的 this 指向,统一操作 API。
十三、总结(源码级)
-
Vue3 响应式的本质是 Proxy + 依赖收集 + 触发更新
-
核心数据结构是 WeakMap → Map → Set
-
track建立关联,trigger触发执行 -
手写一遍,比看十遍源码都有用
📢 下期预告
👉 第 26 篇:Virtual DOM 与 Diff 算法 ------ Vue3 的高效更新机制