Vue 响应式系统
题目描述
实现一个简化版的 Vue 响应式系统,包含:
reactive(obj): 将普通对象变成响应式对象effect(fn): 注册副作用函数,当响应式数据变化时自动执行
本题考查 响应式编程、Proxy 代理 和 依赖追踪 的核心原理。
核心知识点
1. 响应式系统核心原理
- 依赖收集 (Dependency Collection): 在访问属性时记录哪些函数依赖了这个属性
- 派发更新 (Trigger Update): 在修改属性时通知所有依赖该属性的函数重新执行
- 自动执行: 数据变化自动触发相关计算和视图更新
2. 设计模式应用
- 观察者模式: 数据是被观察者,effect 函数是观察者
- 代理模式: Proxy 代理对象的属性访问和修改
- 发布订阅模式: 属性变化时发布事件,订阅者自动执行
3. 数据结构设计
WeakMap {
target1: Map { // 对象级别
key1: Set { effect1, effect2 }, // 属性级别 -> 副作用函数集合
key2: Set { effect3 }
},
target2: Map {
key1: Set { effect1 }
}
}代码实现
javascript
// 当前正在执行的副作用函数(用来依赖收集)
let activeEffect = null
// 依赖收集桶 - 三层数据结构
const bucket = new WeakMap()
// 注册副作用函数
export function effect(fn) {
const effectFn = () => {
// 清理函数:避免遗留的依赖关系
cleanup(effectFn)
// 设置当前活跃的副作用函数
activeEffect = effectFn
// 执行用户函数,触发依赖收集
fn()
}
// 用于存储该副作用函数的依赖集合
effectFn.deps = []
// 立即执行一次
effectFn()
}
// 清理函数:移除副作用函数的所有依赖
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
// 依赖收集函数
function track(target, key) {
if (!activeEffect)
return // 没有活跃的副作用函数
// 获取 target 对应的 Map,没有就创建
let depsMap = bucket.get(target)
if (!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
// 获取 key 对应的 Set,没有就创建
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
// 把当前副作用函数加入依赖集合
deps.add(activeEffect)
// 同时将依赖集合添加到副作用函数的 deps 数组中
activeEffect.deps.push(deps)
}
// 派发更新函数
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap)
return
const effects = depsMap.get(key)
if (!effects)
return
// 新建一个 Set 来避免无限循环
const effectsToRun = new Set(effects)
effectsToRun.forEach(effectFn => effectFn())
}
// 创建响应式对象
export function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 依赖收集
track(target, key)
// 返回实际值,使用 Reflect 确保正确的 this 绑定
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
// 设置属性值
const result = Reflect.set(target, key, value, receiver)
// 派发更新
trigger(target, key)
return result
},
})
}关键技术点
1. activeEffect 的作用机制
javascript
// 全局变量,用于标记当前正在执行的副作用函数
const activeEffect = null
effect(() => {
// 执行时 activeEffect 指向这个函数
console.log(user.name) // 访问 user.name 时建立依赖关系
})
// 执行完毕 activeEffect 重置为 null2. WeakMap 的选择原因
javascript
// 使用 WeakMap 而不是 Map 的原因:
// 1. 键必须是对象,符合我们的需求
// 2. 弱引用,当对象被垃圾回收时,相关依赖也会自动清除
// 3. 避免内存泄漏
const bucket = new WeakMap()
// 当 obj 被回收时,bucket 中对应的条目也会被自动清除3. 三层数据结构的必要性
javascript
// 第一层:WeakMap<Object, Map> - 区分不同的响应式对象
// 第二层:Map<string, Set> - 区分对象的不同属性
// 第三层:Set<Function> - 存储依赖该属性的所有副作用函数
WeakMap {
userObj: Map {
'name': Set { effect1, effect2 },
'age': Set { effect3 }
},
postObj: Map {
'title': Set { effect1 }
}
}4. Proxy vs Object.defineProperty
javascript
// Proxy 的优势:
// 1. 可以拦截任意属性的访问,包括新增属性
// 2. 支持数组索引的拦截
// 3. 支持 13 种拦截操作
// 4. 返回新对象,不直接修改原对象
// Object.defineProperty 的局限:
// 1. 只能劫持已存在的属性
// 2. 无法监听数组长度变化
// 3. 需要递归遍历对象的所有属性5. 清理机制的重要性
javascript
function cleanup(effectFn) {
// 必须清理旧的依赖关系,避免内存泄漏
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn) // 从依赖集合中移除该函数
}
effectFn.deps.length = 0 // 清空函数的依赖列表
}6. 常见陷阱和坑点
- 无限循环: 在 effect 中既读取又修改同一个属性
- 依赖遗留: 不清理旧依赖导致的内存泄漏
- this 绑定: 忘记使用 Reflect 导致 this 指向错误
- 嵌套 effect: 嵌套调用时 activeEffect 的覆盖问题
使用示例
javascript
// 基础响应式
const user = reactive({ name: 'Tom', age: 20 })
effect(() => {
console.log('用户名:', user.name) // 建立 name 属性的依赖
})
// 立即输出:用户名: Tom
user.name = 'Jerry' // 触发更新
// 输出:用户名: Jerry
// 多属性依赖
const state = reactive({ count: 0, doubleCount: 0 })
effect(() => {
state.doubleCount = state.count * 2 // 依赖 count,修改 doubleCount
})
effect(() => {
console.log(`Count: ${state.count}, Double: ${state.doubleCount}`)
})
state.count = 5
// 输出:Count: 5, Double: 10
// 条件依赖
const data = reactive({ show: true, name: 'Vue', age: 6 })
effect(() => {
console.log(data.show ? data.name : data.age)
})
// 初始输出:Vue (建立 show 和 name 的依赖)
data.age = 7 // 不会触发更新,因为当前不依赖 age
data.show = false // 触发更新,输出:7 (现在依赖 age 而不是 name)
data.name = 'React' // 不会触发更新,因为现在不依赖 name
// 对象嵌套
const nested = reactive({
user: { name: 'Alice', profile: { age: 25 } },
})
effect(() => {
console.log(nested.user.name) // 只对 user 属性建立依赖
})
nested.user = { name: 'Bob', profile: { age: 30 } } // 触发更新
nested.user.name = 'Carol' // 不会触发更新(需要深度响应式)
// 数组操作
const list = reactive(['a', 'b', 'c'])
effect(() => {
console.log('列表长度:', list.length)
})
list.push('d') // 触发更新
list[0] = 'A' // 如果实现了数组响应式,也会触发更新记忆要点
核心记忆点
- 依赖收集时机 - effect 执行时访问响应式属性
- 更新触发时机 - 响应式属性被修改时
- 三层数据结构 - 对象 → 属性 → 副作用函数集合
- activeEffect 机制 - 全局标记当前执行的副作用函数
- 清理机制 - 避免依赖泄漏和无限循环
执行流程记忆
javascript
// 注册阶段
effect(fn) → activeEffect = fn → fn() → 访问属性 → track() → 建立依赖
// 更新阶段
修改属性 → set 拦截 → trigger() → 查找依赖 → 执行所有依赖函数扩展思考
1. 深度响应式实现
javascript
function reactive(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj
}
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
const res = Reflect.get(target, key, receiver)
// 深度响应式:如果访问的是对象,递归创建响应式
if (typeof res === 'object' && res !== null) {
return reactive(res)
}
return res
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key)
return result
},
})
}2. 计算属性实现
javascript
function computed(getter) {
let value
let dirty = true // 标记是否需要重新计算
const effectFn = effect(() => {
value = getter()
dirty = false
})
return {
get value() {
if (dirty) {
effectFn()
}
return value
},
}
}
// 使用示例
const state = reactive({ count: 1 })
const doubleCount = computed(() => state.count * 2)
console.log(doubleCount.value) // 2
state.count = 3
console.log(doubleCount.value) // 63. 异步更新和调度器
javascript
// 任务队列,避免同步更新造成的性能问题
const jobQueue = new Set()
const p = Promise.resolve()
let isFlushing = false
function flushJob() {
if (isFlushing)
return
isFlushing = true
p.then(() => {
jobQueue.forEach(job => job())
jobQueue.clear()
isFlushing = false
})
}
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
fn()
}
effectFn.deps = []
effectFn.options = options
if (!options.lazy) {
effectFn()
}
return effectFn
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap)
return
const effects = depsMap.get(key)
if (!effects)
return
const effectsToRun = new Set(effects)
effectsToRun.forEach((effectFn) => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
}
else {
effectFn()
}
})
}
// 使用调度器实现异步更新
effect(
() => {
console.log(state.count)
},
{
scheduler(fn) {
jobQueue.add(fn)
flushJob()
},
},
)4. 支持数组方法的响应式
javascript
const arrayInstrumentations = {};
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(
(method) => {
arrayInstrumentations[method] = function (...args) {
// 暂停追踪,避免 length 属性的依赖收集
shouldTrack = false
// 调用原始方法
const res = Array.prototype[method].apply(this, args)
// 恢复追踪
shouldTrack = true
return res
}
},
)
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 如果是数组且访问的是重写的方法
if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
return Reflect.get(arrayInstrumentations, 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 (oldValue !== value) {
trigger(target, key)
}
return result
},
})
}5. ref 和 reactive 的区别
javascript
function ref(value) {
const wrapper = {
value,
}
// 定义一个不可枚举的属性,用于标识 ref
Object.defineProperty(wrapper, '__v_isRef', {
value: true,
})
return reactive(wrapper)
}
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key]
},
set value(val) {
obj[key] = val
},
}
Object.defineProperty(wrapper, '__v_isRef', {
value: true,
})
return wrapper
}
function toRefs(obj) {
const ret = {}
for (const key in obj) {
ret[key] = toRef(obj, key)
}
return ret
}