深入 Vue3 响应式原理:从 Proxy 到依赖收集的完整实现

1380 字
7 分钟
深入 Vue3 响应式原理:从 Proxy 到依赖收集的完整实现

深入 Vue3 响应式原理:从 Proxy 到依赖收集的完整实现#

Vue3 的响应式系统是整个框架的核心,相比 Vue2 的 Object.defineProperty,Vue3 使用 Proxy 实现了更强大、更完整的响应式能力。本文将从零手写一个简化版的 Vue3 响应式系统,深入理解其底层原理。

一、为什么要用 Proxy 替代 defineProperty?#

Vue2 的 Object.defineProperty 存在几个致命缺陷:

// Vue2 无法检测到以下变化
const obj = { a: 1 }
obj.b = 2 // ❌ 新增属性无法响应
delete obj.a // ❌ 删除属性无法响应
arr[0] = 'new' // ❌ 通过索引修改数组无法响应
arr.length = 0 // ❌ 修改 length 无法响应

Proxy 可以拦截对象的几乎所有操作:

const handler = {
get(target, key, receiver) { /* 读取拦截 */ },
set(target, key, value, receiver) { /* 设置拦截 */ },
deleteProperty(target, key) { /* 删除拦截 */ },
has(target, key) { /* in 操作符拦截 */ },
ownKeys(target) { /* for...in 拦截 */ }
}

二、核心实现:reactive 和 effect#

2.1 全局依赖收集容器#

// 当前正在执行的 effect
let activeEffect = null
// effect 栈,处理嵌套 effect
const effectStack = []
// 存储依赖关系:target -> key -> effects
const targetMap = new WeakMap()
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.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)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
if (!effects) return
// 创建新 Set 避免无限循环
const effectsToRun = new Set()
effects.forEach(effect => {
// 避免自身触发自身
if (effect !== activeEffect) {
effectsToRun.add(effect)
}
})
effectsToRun.forEach(effect => {
// 如果有调度器,使用调度器执行
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
})
}

2.2 reactive 实现#

const reactiveMap = new WeakMap()
function reactive(target) {
// 避免重复代理
if (reactiveMap.has(target)) {
return reactiveMap.get(target)
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 标记已被代理
if (key === '__v_isReactive') return true
const result = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
// 深层代理:惰性转换
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 值变化时才触发
if (oldValue !== value && (oldValue === oldValue || value === value)) {
trigger(target, key)
}
return result
},
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
}
})
reactiveMap.set(target, proxy)
return proxy
}

2.3 effect 实现#

function effect(fn, options = {}) {
const effectFn = () => {
// 清除旧依赖,避免分支切换导致的遗留依赖
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
const result = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return result
}
effectFn.deps = []
effectFn.options = options
if (!options.lazy) {
effectFn()
}
return effectFn
}
function cleanup(effectFn) {
for (const deps of effectFn.deps) {
deps.delete(effectFn)
}
effectFn.deps.length = 0
}

三、进阶:computed 的实现#

computed 本质上是一个带有缓存和惰性求值的 effect

function computed(getter) {
let value
let dirty = true // 脏标记
const effectFn = effect(getter, {
lazy: true,
scheduler() {
if (!dirty) {
dirty = true
// 手动触发依赖更新
trigger(obj, 'value')
}
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
// 收集外层依赖
track(obj, 'value')
return value
}
}
return obj
}

使用示例#

const state = reactive({ price: 100, quantity: 3 })
const total = computed(() => {
console.log('computed 执行')
return state.price * state.quantity
})
// 第一次访问,执行 getter
console.log(total.value) // computed 执行 → 300
// 第二次访问,命中缓存
console.log(total.value) // 300(不打印 "computed 执行")
// 修改依赖,标记为 dirty
state.price = 200
// 再次访问,重新计算
console.log(total.value) // computed 执行 → 600

四、watch 的实现#

watch 基于 effect + scheduler 实现:

function watch(source, callback, options = {}) {
let getter
if (typeof source === 'function') {
getter = source
} else {
// 递归读取所有属性,触发 track
getter = () => traverse(source)
}
let oldValue, newValue
let cleanup
function onInvalidate(fn) {
cleanup = fn
}
const job = () => {
newValue = effectFn()
// 过期回调,用于竞态处理
if (cleanup) cleanup()
callback(newValue, oldValue, onInvalidate)
oldValue = newValue
}
const effectFn = effect(getter, {
lazy: true,
scheduler: () => {
// flush: 'post' 时放入微任务队列
if (options.flush === 'post') {
Promise.resolve().then(job)
} else {
job()
}
}
})
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
function traverse(value, seen = new Set()) {
if (typeof value !== 'object' || value === null || seen.has(value)) {
return value
}
seen.add(value)
for (const key in value) {
traverse(value[key], seen)
}
return value
}

竞态问题处理#

watch(
() => state.id,
async (newId, oldId, onInvalidate) => {
let expired = false
onInvalidate(() => { expired = true })
const data = await fetchData(newId)
// 如果已过期(新请求已发出),忽略结果
if (!expired) {
state.data = data
}
}
)

五、Vue3 响应式的性能优化#

5.1 惰性代理#

Vue3 不会一次性将所有嵌套对象转为响应式,而是在访问时才代理:

// get 中的惰性转换
if (typeof result === 'object' && result !== null) {
return reactive(result) // 访问时才代理
}

5.2 位运算标记#

Vue3 源码中大量使用位运算来标记组件状态:

const enum ShapeFlags {
ELEMENT = 1, // 0001
STATEFUL_COMPONENT = 1 << 2, // 0100
TEXT_CHILDREN = 1 << 3, // 1000
ARRAY_CHILDREN = 1 << 4, // 10000
}
// 判断类型:位与
if (vnode.shapeFlag & ShapeFlags.ELEMENT) { /* ... */ }
// 组合类型:位或
vnode.shapeFlag = ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN

5.3 调度器批量更新#

const queue = new Set()
let isFlushing = false
function queueJob(job) {
queue.add(job)
if (!isFlushing) {
isFlushing = true
Promise.resolve().then(() => {
queue.forEach(job => job())
queue.clear()
isFlushing = false
})
}
}

总结#

Vue3 响应式系统的核心设计思想:

特性实现方式
数据拦截Proxy + Reflect
依赖收集WeakMap → Map → Set
副作用管理effect + effectStack
缓存计算computed (lazy + dirty)
异步调度scheduler + 微任务队列
竞态处理onInvalidate 回调

理解这些底层原理,不仅能帮你更好地使用 Vue3,也能在遇到响应式相关 Bug 时快速定位问题。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

深入 Vue3 响应式原理:从 Proxy 到依赖收集的完整实现
https://boke.hackerdream.xyz/posts/vue3-reactivity-deep-dive/
作者
晴天
发布于
2025-12-29
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录