
文章目录
- Proxy与Reflect详解(二):进阶篇------手写Vue3响应式系统
Proxy与Reflect详解(二):进阶篇------手写Vue3响应式系统
概念概述
上一篇我们掌握了Proxy和Reflect的基础语法。这一篇我们深入Vue3源码,从零手写一个迷你响应式系统 ,理解reactive()、ref()、effect()、computed()的底层原理。
Vue3响应式的核心只有四个字:收集依赖、触发更新。
想象一个订阅系统:
- track(收集依赖):谁用了这个数据,就登记到"订阅列表"里
- trigger(触发更新):数据变了,通知所有订阅者重新执行
核心原理:依赖收集与触发更新
整体架构
┌─────────────────────────────────────────────────────────────────┐
│ Vue3 响应式系统架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ reactive(obj) │
│ ↓ │
│ Proxy代理 ──→ get拦截 ──→ track(target, key) 依赖收集 │
│ │ set拦截 ──→ trigger(target, key) 触发更新 │
│ ↓ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ targetMap (WeakMap) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ target → depsMap (Map) │ │ │
│ │ │ ┌────────────────────────────────────────┐ │ │ │
│ │ │ │ key → dep (Set<ReactiveEffect>) │ │ │ │
│ │ │ └────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ effect(fn) │
│ ↓ │
│ ReactiveEffect实例 │
│ ↓ │
│ 执行fn → 触发get → track收集依赖 → 数据变化时重新执行fn │
│ │
└─────────────────────────────────────────────────────────────────┘
依赖存储结构(三级Map)
targetMap: WeakMap<object, Map<string, Set<ReactiveEffect>>>
第1层:WeakMap key=目标对象target,value=depsMap
第2层:Map key=属性名, value=dep(依赖集合)
第3层:Set 存储所有依赖这个属性的 ReactiveEffect 实例
示例:
targetMap = {
{ name: 'Alice', age: 25 } => {
'name' => Set([effect1, effect2]), // name被effect1和effect2使用
'age' => Set([effect1]) // age只被effect1使用
}
}
完整可运行代码:手写迷你Vue3响应式系统
以下是完整可运行的迷你实现 ,约150行代码,覆盖reactive、ref、effect、computed核心功能:
javascript
// ============================================================
// 第一部分:核心依赖管理
// ============================================================
// 全局变量:当前正在执行的effect(依赖收集的关键)
let activeEffect = null
// 依赖存储:WeakMap(target) → Map(key) → Set(effects)
const targetMap = new WeakMap()
// 收集依赖:记录"谁在读取哪个对象的哪个属性"
function track(target, key) {
// 如果没有正在执行的effect,不需要收集
if (!activeEffect) return
// 第1层:取出target对应的depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 第2层:取出key对应的dep集合
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
// 第3层:将当前effect加入依赖集合
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
// 反向收集:effect知道自己依赖了哪些dep(用于清理)
activeEffect.deps.push(dep)
}
}
// 触发更新:通知所有依赖"数据变了"
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (!dep) return
// 拷贝一份再遍历,防止在执行effect时修改dep导致无限循环
const effectsToRun = new Set(dep)
effectsToRun.forEach(effect => {
// 避免effect触发自身导致无限递归
if (effect !== activeEffect) {
effect.run()
}
})
}
// ============================================================
// 第二部分:ReactiveEffect 类
// ============================================================
class ReactiveEffect {
constructor(fn) {
this.fn = fn // 用户传入的副作用函数
this.deps = [] // 反向依赖列表(用于清理)
}
run() {
// 将自己设为当前活跃的effect
const prevEffect = activeEffect
activeEffect = this
try {
// 执行副作用函数,执行过程中会触发get → track收集依赖
return this.fn()
} finally {
// 执行完毕后恢复上一个effect
activeEffect = prevEffect
}
}
stop() {
// 清理所有依赖
this.deps.forEach(dep => dep.delete(this))
this.deps.length = 0
}
}
// ============================================================
// 第三部分:reactive() ------ 创建响应式对象
// ============================================================
// 缓存:避免同一个对象被代理多次
const reactiveMap = new WeakMap()
// 用于判断是否是代理对象的标记
const RAW_KEY = Symbol('__raw__')
const IS_REACTIVE = Symbol('__isReactive__')
function reactive(target) {
// 只能代理对象
if (typeof target !== 'object' || target === null) {
return target
}
// 已经是代理对象,直接返回
if (target[IS_REACTIVE]) {
return target
}
// 已经代理过,返回缓存
if (reactiveMap.has(target)) {
return reactiveMap.get(target)
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 通过特殊key获取原始对象
if (key === RAW_KEY) return target
// 判断是否是reactive代理
if (key === IS_REACTIVE) 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) {
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
const hadKey = key in target
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
trigger(target, key)
}
return result
}
})
reactiveMap.set(target, proxy)
return proxy
}
// ============================================================
// 第四部分:ref() ------ 包装基本类型为响应式
// ============================================================
function ref(value) {
// ref本质就是一个{ value: xxx }的reactive对象
return reactive({ value })
}
// ============================================================
// 第五部分:effect() ------ 注册副作用函数
// ============================================================
function effect(fn) {
const reactiveEffect = new ReactiveEffect(fn)
// 立即执行一次,触发依赖收集
reactiveEffect.run()
// 返回runner函数,可以手动触发
const runner = reactiveEffect.run.bind(reactiveEffect)
runner.effect = reactiveEffect
return runner
}
// ============================================================
// 第六部分:computed() ------ 计算属性(带缓存)
// ============================================================
function computed(getter) {
// 缓存标记
let dirty = true
let cachedValue
// 用effect监听getter的依赖变化
const runner = effect(() => {
// 依赖变化时,标记为脏
dirty = true
return getter()
})
// 返回一个只读的响应式对象
return {
get value() {
if (dirty) {
// 只有脏了才重新计算
cachedValue = runner()
dirty = false
}
return cachedValue
}
}
}
核心特性详解
特性1:依赖收集的时机
javascript
// 依赖收集发生在effect执行时,触发get的那一刻
const state = reactive({ count: 0 })
effect(() => {
// 执行到这里,读取state.count
// → 触发Proxy的get拦截
// → track(state, 'count')将当前effect加入dep
console.log('count is:', state.count)
})
// 此时依赖关系:
// targetMap: {
// state(原始对象) → {
// 'count' → Set([上面那个effect])
// }
// }
特性2:依赖是动态的
javascript
const state = reactive({
ok: true,
text: 'hello'
})
effect(() => {
// 条件分支:只读取了ok
console.log(state.ok ? state.text : 'world')
})
// 初始:effect同时依赖ok和text
// 如果把ok改为false,effect只依赖ok,不再依赖text
// → Vue3会在每次effect执行前清理旧依赖,重新收集
特性3:ref解包机制
javascript
const count = ref(0)
// ref在reactive中自动解包
const state = reactive({ count })
console.log(state.count) // 0(自动解包,不需要.value)
console.log(state.count.value) // undefined(没有这个属性)
完整使用示例
javascript
// ============================================================
// 完整测试:验证手写的响应式系统
// ============================================================
console.log('===== 测试1:reactive基本响应 =====')
const state = reactive({ count: 0, name: 'Alice' })
effect(() => {
console.log(`[effect1] count = ${state.count}`)
})
// 输出: [effect1] count = 0
state.count++
// 输出: [effect1] count = 1
state.count++
// 输出: [effect1] count = 2
console.log('\n===== 测试2:多个effect =====')
const user = reactive({ name: 'Alice', age: 25 })
effect(() => console.log(`[effect-A] name = ${user.name}`))
effect(() => console.log(`[effect-B] age = ${user.age}`))
// 输出: [effect-A] name = Alice
// 输出: [effect-B] age = 25
user.name = 'Bob'
// 输出: [effect-A] name = Bob
user.age = 30
// 输出: [effect-B] age = 30
console.log('\n===== 测试3:ref响应 =====')
const count = ref(0)
effect(() => {
console.log(`[ref-effect] count = ${count.value}`)
})
// 输出: [ref-effect] count = 0
count.value++
// 输出: [ref-effect] count = 1
console.log('\n===== 测试4:computed计算属性 =====')
const price = ref(100)
const quantity = ref(3)
const total = computed(() => {
console.log('[computed] 重新计算total')
return price.value * quantity.value
})
console.log('total:', total.value)
// 输出: [computed] 重新计算total
// 输出: total: 300
console.log('total:', total.value)
// 输出: total: 300 (没有重新计算,使用缓存!)
quantity.value = 5
// 输出: [computed] 重新计算total(依赖变化,标记为脏)
console.log('total:', total.value)
// 输出: total: 500
console.log('\n===== 测试5:嵌套对象响应 =====')
const nested = reactive({
user: {
profile: {
name: 'Alice'
}
}
})
effect(() => {
console.log(`[nested] name = ${nested.user.profile.name}`)
})
// 输出: [nested] name = Alice
nested.user.profile.name = 'Bob'
// 输出: [nested] name = Bob
console.log('\n===== 测试6:数组响应 =====')
const list = reactive([1, 2, 3])
effect(() => {
console.log(`[array] length = ${list.length}, items = ${list.join(',')}`)
})
// 输出: [array] length = 3, items = 1,2,3
list.push(4)
// 输出: [array] length = 4, items = 1,2,3,4
list[0] = 100
// 输出: [array] length = 4, items = 100,2,3,4
console.log('\n===== 测试7:delete响应 =====')
const config = reactive({ debug: true, verbose: false })
effect(() => {
console.log(`[delete-test] debug exists: ${'debug' in config}`)
})
// 输出: [delete-test] debug exists: true
delete config.debug
// 输出: [delete-test] debug exists: false
开发坑点
坑点1:effect中的条件依赖导致更新遗漏
javascript
const state = reactive({ a: 1, b: 2, flag: true })
effect(() => {
if (state.flag) {
console.log(state.a) // flag为true时依赖a
} else {
console.log(state.b) // flag为false时依赖b
}
})
// 问题:flag为true时,effect只依赖flag和a
// 修改b不会触发更新(符合预期,因为b没被使用)
// 但如果我们把flag改为false,effect应该依赖b
// → Vue3的做法:每次effect执行前先清理所有旧依赖,重新收集
坑点2:effect嵌套的执行顺序
javascript
const state = reactive({ count: 0 })
effect(() => {
effect(() => {
console.log('内层:', state.count) // 内层effect先执行
})
console.log('外层:', state.count) // 外层effect后执行
})
// Vue3用effect栈来处理嵌套:
// push effect → 执行内层 → push inner → 执行 → pop → 执行外层 → pop
坑点3:computed的缓存失效时机
javascript
const state = reactive({ items: [1, 2, 3] })
const sum = computed(() => {
// 如果computed内部有副作用,缓存可能导致副作用不执行
console.log('计算中...')
return state.items.reduce((a, b) => a + b, 0)
})
// 只读取value时才计算
console.log(sum.value) // 计算中... 6
// 多次读取不会重新计算
console.log(sum.value) // 6(没有"计算中...")
console.log(sum.value) // 6(没有"计算中...")
// items变化后,下次读取才重新计算
state.items.push(4)
console.log(sum.value) // 计算中... 10
工程实战场景
场景1:简易状态管理(替代Pinia的核心原理)
javascript
// 基于reactive实现一个简易store
function createStore(options) {
const { state, getters, actions } = options
// 1. state用reactive包装
const storeState = reactive(state())
// 2. getters用computed包装
const storeGetters = {}
for (const key in getters) {
storeGetters[key] = computed(() => getters[key](storeState))
}
// 3. actions绑定store上下文
const storeActions = {}
for (const key in actions) {
storeActions[key] = (...args) => {
actions[key].call(store, ...args)
}
}
// 4. 组装store
const store = {
state: storeState,
...storeGetters,
...storeActions
}
return store
}
// 使用
const todoStore = createStore({
state: () => ({
todos: [],
filter: 'all'
}),
getters: {
activeTodos: (state) => state.todos.filter(t => !t.done),
completedTodos: (state) => state.todos.filter(t => t.done),
filteredTodos(state) {
if (state.filter === 'active') return this.activeTodos
if (state.filter === 'completed') return this.completedTodos
return state.todos
}
},
actions: {
addTodo(text) {
this.state.todos.push({ text, done: false, id: Date.now() })
},
toggleTodo(id) {
const todo = this.state.todos.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
}
})
// 响应式使用
effect(() => {
console.log('待办数量:', todoStore.state.todos.length)
})
todoStore.addTodo('学习Vue3响应式') // 触发effect
todoStore.addTodo('写博客') // 触发effect
场景2:表单双向绑定的底层原理
javascript
// 模拟Vue的v-model底层原理
function modelBinding(target, key) {
const state = reactive(target)
// 双向绑定对象
const binding = {
// 读取时收集依赖
get value() {
track(state, key)
return state[key]
},
// 设置时触发更新
set value(newValue) {
state[key] = newValue
}
}
return binding
}
// 使用
const formData = modelBinding({ username: '' }, 'username')
effect(() => {
console.log('当前输入:', formData.value)
})
// 输出: 当前输入:
formData.value = 'Alice' // 触发更新
// 输出: 当前输入: Alice
formData.value = 'Bob'
// 输出: 当前输入: Bob
面试题总结
Q1:track和trigger的作用分别是什么?
核心答案:
track(target, key):依赖收集。在effect执行过程中,当Proxy的get被触发时,将当前activeEffect记录到targetMap的对应位置trigger(target, key):触发更新。当Proxy的set被触发时,从targetMap中取出该属性的所有effect并执行
Q2:为什么用WeakMap而不是Map?
核心答案:WeakMap的key是弱引用。如果target对象被垃圾回收,对应的依赖信息也会自动被回收,避免内存泄漏。如果用Map,即使target不再使用,其依赖信息仍会占用内存。
Q3:computed的缓存是如何实现的?
核心答案 :通过dirty标记控制。首次读取时执行getter并缓存结果,标记dirty=false。依赖变化时(effect中重新执行getter),将dirty标记为true。下次读取时发现dirty=true才重新计算。
Q4:为什么effect执行前要清理旧依赖?
核心答案:因为条件分支会导致依赖是动态的。如果上一次执行依赖了A和B,但这次只依赖A,那么B的dep中应该移除这个effect,否则B变化时会触发不必要的更新。