下面咱们尝试从零实现一个简易版 Vue3 响应式系统,通过五个关键模块搭建一个可运行的迷你响应式系统:
响应式系统架构图
1. 创建依赖管理中心
整体的流程如下:当访问响应式对象的属性时,会调用track函数来收集依赖。当属性被修改时,调用trigger函数触发所有相关的副作用函数effect。
const targetMap = new WeakMap() // 存储所有响应式对象的依赖关系,形成 `target -> key -> effects` 的树形结构
let activeEffect = null // 记录当前激活的副作用
// 依赖收集
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) // 收集当前副作用
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect()) // 触发所有关联副作用
}
}
2. 实现 reactive 核心代理
使用Proxy来拦截对象的get和set操作。
当访问响应式对象的属性时,会触发get陷阱,进行依赖收集;当修改对象属性时,触发set陷阱,检查值是否变化,然后触发更新。这样,当数据变化时,之前收集的effect会被执行,从而实现响应式更新。
// 函数接收一个对象obj,返回一个Proxy实例
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 oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 只有值变化时才触发更新
if (!Object.is(oldValue, value)) {
trigger(target, key)
}
return result
}
})
}
为什么用 Reflect.get/set?
保持 this 指向正确性:
使用 Reflect 方法确保在对象有继承关系时,receiver(即 Proxy 实例)能正确传递,避免 this 指向原始对象导致响应式丢失
// 示例:若不用 Reflect,可能导致问题
const parent = { a: 1 }
const child = reactive({ __proto__: parent })
child.a // 此时 receiver 应该是 child(Proxy),而非 parent
为什么用 Object.is?
比 === 更严格,能正确处理 NaN 的特殊情况
Object.is(NaN, NaN) // true
NaN === NaN // false
3. 实现副作用注册机制
function effect(fn) {
activeEffect = fn // 挂载当前副作用
fn() // 触发依赖收集
activeEffect = null // 清除激活状态,防止后续非effect触发的属性访问造成误收集
}
// 使用示例
const state = reactive({ count: 0 })
effect(() => {
console.log('count changed:', state.count)
})
4. 实现 ref 的响应式包装
Vue3中ref的作用通常用于包装基本类型值(如数字、字符串等),使其成为响应式对象。因为Vue3的reactive使用Proxy,而Proxy只能代理对象,无法直接处理基本类型。所以ref通过创建一个带有value属性的对象来包裹原始值,从而实现对基本类型的响应式支持。
function ref(value) {
return reactive({
get value() {
track(this, 'value')
return value
},
set value(newVal) {
if (!Object.is(value, newVal)) {
value = newVal
trigger(this, 'value')
}
}
})
}
// 自动解包测试
const num = ref(42)
effect(() => {
console.log('num is:', num.value) // 自动追踪.value访问
})
ref 与 reactive 的协作关系
| 特性 | ref | reactive |
|---|---|---|
| 适用类型 | 基本类型 + 对象 | 仅对象 |
| 内存开销 | 额外包装对象 | 直接代理原对象 |
| 访问方式 | .value 访问 | 直接属性访问 |
| 追踪粒度 | 整个 value 属性 | 每个独立属性 |
| 嵌套代理 | 传入对象时二次代理 | 自动浅层代理 |
性能优化技巧
// 避免重复代理对象
const obj = reactive({ data: null })
const data = ref(obj) // 内部会再次代理 obj → 非必要性能损耗
// 正确用法
const data = ref(null) // 基本类型无需担心
data.value = obj // 赋值已代理对象
5. 嵌套对象处理
reactive函数使用Proxy来创建响应式对象。核心在于get拦截器里的递归处理。当访问对象属性时,如果属性的值是对象,就递归调用reactive,确保嵌套对象也是响应式的,优化如下:
function reactive(obj) {
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 递归代理嵌套对象 ↓↓↓ 核心代码
if (typeof res === 'object' && res !== null) {
return reactive(res) // 自动深度代理
}
return res
},
// ...其他拦截器保持不变
})
return observed
}
// 测试嵌套响应
const state = reactive({
user: { name: 'John' }
})
effect(() => {
console.log('User name:', state.user.name) // 自动追踪嵌套属性
})
// 原始情况:
state.name.isProxy → undefined // 嵌套对象未被代理
// 改进后:
state.name.isProxy → true // 自动深度代理
ps:实际Vue3源码使用WeakMap缓存代理对象
性能影响分析
| 操作类型 | 无缓存处理 | 有缓存处理 |
|---|---|---|
| 首次访问属性 | 创建新代理对象 | 创建并缓存代理对象 |
| 重复访问属性 | 反复创建相同代理 | 直接返回缓存代理 |
| 内存占用 | 可能内存泄漏 | WeakMap自动回收 |
| CPU消耗 | 高频访问时消耗较大 | 一次代理多次使用 |
特殊场景处理
- 循环引用问题
const obj = { a: null }
obj.a = obj // 创建循环引用
const proxyObj = reactive(obj)
// 无保护措施会导致无限递归 ↓
console.log(proxyObj.a.a.a) // 触发Maximum call stack size exceeded
- 🛡️ 防御方案:代理前添加已处理对象检查
function reactive(obj) {
if (obj.__isReactive) return obj // 标记已处理对象
// ...代理逻辑
observed.__isReactive = true
}
- DOM元素处理
reactive({
el: document.getElementById('app')
})
// 代理原生DOM对象可能导致意外行为
- ⚠️ 最佳实践:Vue3源码会跳过对原生对象的代理
⚡ 核心原理验证测试
// 测试用例
const state = reactive({ count: 0 })
const arr = reactive([1, 2, 3])
effect(() => {
console.log('State count:', state.count)
})
effect(() => {
console.log('Array length:', arr.length)
})
// 触发更新
state.count++
arr.push(4) // 只能捕获直接属性修改,无法拦截 push 这类方法对 length 的隐式修改,留个思考:该如何优化?
arr[0] = 9 // 索引修改,这里无 effect 依赖该索引,不触发