4.2 响应式数据的基本实现
用
Proxy的 get/set 把「读取时收集、设置时触发」跑通,得到一个最小可用的响应式系统,并看清它的两处硬伤。
一、最小实现
// 存储副作用函数的「桶」
const bucket = new Set()
// 原始数据
const data = { text: 'hello world' }
const obj = new Proxy(data, {
// 拦截读取:收集副作用函数
get(target, key) {
bucket.add(effect) // 把 effect 放进桶里
return target[key]
},
// 拦截设置:触发副作用函数
set(target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => fn()) // 取出桶里的函数挨个执行
return true // 返回 true 代表设置成功
}
})
function effect() {
document.body.innerText = obj.text
}
effect() // 先执行一次 → 触发读取 → 完成收集
setTimeout(() => {
obj.text = 'hello vue3' // 1 秒后修改 → 副作用函数重新执行
}, 1000)
三步流程:
effect()首次执行 → 读到obj.text→get拦截把effect收进bucket;obj.text = 'hello vue3'→set拦截 → 桶里的函数全部重新执行;effect重新执行时再次读取obj.text,页面被更新。
二、两个明显的缺陷
- 依赖函数名硬编码:
bucket.add(effect)写死了名字,副作用函数一旦是匿名函数就收集不到; - 不区分属性名:桶只是单层
Set,obj.notExist = 'x'这种「压根没被读取过」的赋值,照样会触发所有副作用函数。
三、先解决匿名函数:activeEffect 注册机制
用一个全局变量 activeEffect 记录「当前正在执行的副作用函数」,并提供 effect() 注册函数:
// 全局变量:存储当前被注册的副作用函数
let activeEffect
function effect(fn) {
activeEffect = fn // 注册时先记住它
fn() // 再执行;执行期间的读取都算它的依赖
}
const obj = new Proxy(data, {
get(target, key) {
if (activeEffect) {
bucket.add(activeEffect) // 收集的是「当前」的副作用函数
}
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => fn())
return true
}
})
effect(() => {
console.log('effect run')
document.body.innerText = obj.text
})
// 副作用函数中并没有读取 notExist 属性
obj.notExist = 'hello vue3' // 仍然触发了副作用函数 → 错误
只换掉注册方式还不够:桶无法表达「哪个 target 的哪个 key 依赖了哪些副作用函数」,所以任何 key 变化都会触发全部函数。这正是下一节要重构桶结构的原因。
相关
- 4.1 响应式数据与副作用函数:先弄清副作用函数与响应式数据的概念
- 4.3 设计一个完善的响应系统:把单层 Set 桶升级为
WeakMap → Map → Set