🚀 深入浅出 Vue 3 响应式系统:从零构建核心 Reativity
本文将基于 Vue 3 的设计思想,带您从零开始,逐步构建一个完整的 Proxy 响应式系统。我们将拆解您的源码,详细解析每个步骤背后的 底层问题 与 解决方案,确保您不仅学会如何实现,更能理解其工作原理。
一、系统基石:初始化与状态管理
在开始拦截数据操作之前,我们必须建立响应式系统的核心环境和数据存储结构。
1. 核心状态(activeEffect 与 effectStack)
这是处理副作用函数执行环境的关键。
let activeEffect = null // 💥 谁在使用数据?当前正在运行的 effect 函数
const effectStack = [] // 用于处理 effect 嵌套的栈
const TRANCK_KEY = Symbol() // 专用于 for...in / Object.keys() 依赖收集的键
2. 依赖存储桶(bucket)
所有依赖关系都存储在一个多层嵌套的结构中。
const bucket = new WeakMap()
const triggerType = {
SET: 'SET', // 修改属性值 (key 已存在)
ADD: 'ADD', // 新增属性 (key 不存在)
DELETE: 'DELETE', // 删除属性
}
WeakMap:作为顶层存储,以响应式对象作为键。它的优势在于,如果对象在外部被垃圾回收,bucket中对应的依赖也会自动清除,有效避免了 内存泄漏。
二、核心驱动:effect(副作用函数)与依赖清理
effect 函数是响应式逻辑的入口,它定义了**“要做什么”**。高效的依赖清理是确保系统正确性的基石。
1. 实现 effect 函数
它负责设置执行环境,确保在 fn() 运行时能正确进行依赖收集。
function effect(fn, options = {}) {
function effectFn() {
clean(effectFn) // 1. 清理旧依赖
effectStack.push(effectFn) // 2. 入栈
activeEffect = effectFn // 3. 设置当前活跃 effect
const res = fn() // 4. 执行 fn,触发 get -> track 收集依赖
effectStack.pop() // 5. 出栈
activeEffect = effectStack[effectStack.length - 1] // 6. 恢复上一个 effect
return res
}
effectFn.deps = [] // 反向存储依赖集合,用于 clean
effectFn.options = options
if (!options.lazy) {
effectFn() // 默认立即执行一次
}
return effectFn
}
2. 解决动态依赖问题:clean
问题: 如果 effect 内部使用了 if 语句(分支切换),我们必须在下次执行时移除旧的分支依赖。否则,修改不再需要的属性也会导致视图更新。
解决方案: 在 effectFn 重新执行前,先调用 clean,彻底移除它在所有依赖集合中的记录。
function clean(effectFn) {
if (!effectFn || !effectFn.deps) return
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn) // 从 Set 集合中移除 effect
}
effectFn.deps.length = 0 // 清空 effectFn 自身的引用
}
三、构建桥梁:track(收集)与 trigger(派发)
这组函数是响应式系统的中枢神经,连接着数据访问和副作用函数的执行。
1. 依赖收集:track
当属性被 get 访问时调用。核心是将当前的 activeEffect 存储到 bucket 中。
function track(target, key) {
if (!activeEffect) return
// 1. 获取 target 对应的 depsMap
let depsMap = bucket.get(target)
if (!depsMap) { /* 初始化 depsMap */ }
// 2. 获取 key 对应的 deps (Set 集合)
let deps = depsMap.get(key)
if (!deps) { /* 初始化 deps */ }
// 3. 收集 activeEffect (Set 自动去重)
deps.add(activeEffect)
// 4. 反向收集引用,供 clean 时使用
activeEffect.deps.push(deps)
}
2. 派发更新:trigger
当属性被 set 或 delete 时调用。核心是找到所有相关的 effect 并执行它们。
function trigger(target, key, type) {
const depsMap = bucket.get(target)
if (!depsMap) return
const deps = depsMap.get(key)
const effects = new Set(deps) // ⚡️ 优化:拷贝 Set,避免遍历期间集合突变
// 💥 迭代操作依赖的处理(在第五节详解)
if ([triggerType.ADD, triggerType.DELETE].includes(type)) {
effects.add(...(depsMap.get(TRANCK_KEY) || []))
}
effects &&
effects.forEach((fn) => {
// ⚡️ 优化:避免无限递归循环 (如 effect 内部修改自身依赖属性)
if (fn !== activeEffect) {
if (fn.options.scheduler) {
fn.options.scheduler(fn) // 使用调度器(如 nextTick)
} else {
fn()
}
}
})
}
四、核心拦截:处理属性 get 与 set
我们使用 Proxy 的 get 和 set 拦截器来介入对象的读写操作。
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
get(target, key, receiver) {
if (key === 'raw') return target
if (!isReadonly) {
track(target, key) // 读:收集依赖
}
const result = Reflect.get(target, key, receiver)
// 递归代理实现深响应式
if (!isShallow && result !== null && typeof result === 'object') {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
if (isReadonly) { /* 警告并返回 */ }
// ⚡️ 优化:避免原型链继承属性时的重复触发
if (receiver.raw !== target) return true
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 检查值是否变化 (包含 NaN 处理)
if (value !== oldValue && (value === value || oldValue === oldValue)) {
// 判断是新增还是修改
const type = target.hasOwnProperty(key) ? triggerType.SET : triggerType.ADD
trigger(target, key, type) // 写:派发更新
}
return result
},
// ... 其他拦截器
})
}
🎯 何时触发?(get/set)
| 场景 | 触发类型 | 需重新运行的 Effect | 关键点 |
|---|---|---|---|
| 修改 现有属性 | SET | 依赖该 key 的所有 Effect | 最常见的更新场景。 |
| 新增 属性 | ADD | 依赖该 key 的 Effect + 依赖 TRANCK_KEY 的 Effect | 新增属性不仅影响该 key 的访问,也改变了对象的 结构。 |
五、拦截迭代操作:in 与 for...in
对于 Vue 响应式系统,我们不仅要拦截属性的读写,还要拦截判断属性是否存在和遍历属性的行为。
1. 拦截 in 操作符(has)
目标: 使 if ('prop' in obj) 具有响应性。
// ... 在 createReactive 的 Proxy 选项中添加:
has(target, key, receiver) {
track(target, key) // 依赖该 key 的存在性
return Reflect.has(target, key, receiver)
}
2. 拦截 for...in 循环(ownKeys)
目标: 使 for (let k in obj) 在对象结构变化时(属性增删)能重新运行。
// ... 在 createReactive 的 Proxy 选项中添加:
ownKeys(target) {
// 不依赖于任何特定属性,而是依赖于对象的“键列表”
track(target, TRANCK_KEY)
return Reflect.ownKeys(target)
}
3. 拦截属性删除(deleteProperty)
目标: 处理 delete obj.key,并确保 TRANCK_KEY 依赖得到更新。
JavaScript
// ... 在 createReactive 的 Proxy 选项中添加:
deleteProperty(target, key) {
if (isReadonly) return true
const hasKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)
if (result && hasKey) {
trigger(target, key, triggerType.DELETE) // 💥 派发 DELETE 类型更新
}
return result
}
🎯 何时触发?(has/ownKeys/delete)
| 操作 | 触发类型 | 需重新运行的 Effect | 关键点 |
|---|---|---|---|
| 新增 属性 | ADD | 影响 key 的 effect + 影响 TRANCK_KEY 的 effect | 改变了 in 的结果和 for...in 列表。 |
| 删除 属性 | DELETE | 影响 key 的 effect + 影响 TRANCK_KEY 的 effect | 改变了 in 的结果和 for...in 列表。 |
最终 API 导出
通过组合 isShallow 和 isReadonly 参数,我们导出了 Vue 提供的所有响应式 API。
function reactive(obj) { return createReactive(obj, false, false) }
function shallowReactive(obj) { return createReactive(obj, true, false) }
function readonly(obj) { return createReactive(obj, false, true) }
function shallowReadonly(obj) { return createReactive(obj, true, true) }
export {
reactive,
effect,
// ... 其他导出的 track, trigger, shallowReactive, etc.
}
这份文档清晰地展现了 Vue 3 响应式系统如何通过 Proxy 构建一个精确、高效且无副作用的 观察者模式,实现了我们对数据的完全控制。最终源码如下:
let activeEffect = null
const bucket = new WeakMap()
const effectStack = []
const TRANCK_KEY = Symbol() // for in ownKeys
const triggerType = {
SET: 'SET',
ADD: 'ADD',
DELETE: 'DELETE',
}
function effect(fn, options = {}) {
function effectFn() {
clean(effectFn)
effectStack.push(effectFn)
activeEffect = effectFn
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
effectFn.deps = []
effectFn.options = options
if (options.lazy) {
return effectFn
}
effectFn()
}
function clean(effectFn) {
if (!effectFn || !effectFn.deps) return
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
/**
* obj
* 1、get set
* 2、delete
* 3、in操作符
* 4、for in 循环读取
* 5、对象本体和原型链对象都是响应式变量,set的时候,不需要触发两次,虽然在trigger里也有用set兜底
* 6、实现深响应式和浅响应式,深可读和浅可读
*/
/**
*
* @param {*} obj
* @param {*} isShallow 是否浅代理
* @returns
*/
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
get(target, key, receiver) {
if (key === 'raw') {
return target
}
// 非只读才收集依赖
if (!isReadonly) {
track(target, key)
}
const result = Reflect.get(target, key, receiver)
if (!isShallow) {
if (result !== null && typeof result === 'object') {
return reactive(result, isShallow)
}
}
return result
},
set(target, key, value, receiver) {
// 只读给出警告提示
if (isReadonly) {
console.warn(`${key} is readonly`)
return
}
// 避免原型链重复触发
if (receiver.raw !== target) return
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (value !== oldValue && (value === value || oldValue === oldValue)) {
const type = target[key] ? triggerType.SET : triggerType.ADD
trigger(target, key, type)
}
return result
},
has(target, key, receiver) {
track(target, key)
return Reflect.has(target, key, receiver)
},
deleteProperty(target, key) {
if (isReadonly) {
console.warn(`${key} is readonly`)
return
}
const hasKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)
if (result && hasKey) {
trigger(target, key, triggerType.DELETE)
}
return result
},
ownKeys(target) {
track(target, TRANCK_KEY)
return Reflect.ownKeys(target)
},
})
}
function track(target, key) {
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
function trigger(target, key, type) {
const depsMap = bucket.get(target)
if (!depsMap) return
const deps = depsMap.get(key)
const effects = new Set(deps)
if ([triggerType.ADD, triggerType.DELETE].includes(type)) {
effects.add(...(depsMap.get(TRANCK_KEY) || []))
}
effects &&
effects.forEach((fn) => {
if (fn !== activeEffect) {
if (fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
}
})
}
/**
* 创建响应式对象,深响应式且非可读
* @param {*} obj
* @returns
*/
function reactive(obj) {
return createReactive(obj, false, false)
}
/**
* 浅代理
* @param {*} obj
* @returns
*/
function shallowReactive(obj) {
return createReactive(obj, true)
}
/**
* 深可读
* @param {*} obj
* @returns
*/
function readonly(obj) {
return createReactive(obj, false, true)
}
/**
* 浅可读
* @param {*} obj
* @returns
*/
function shallowReadonly(obj) {
return createReactive(obj, true)
}
export {
reactive,
effect,
track,
trigger,
shallowReactive,
readonly,
shallowReadonly,
}