5.3 如何代理 Object
对象的「读取」远不止
obj.foo:in、for...in都是读取,删除则属于另一类操作,它们各自对应一个 Proxy 拦截器。
一、都有哪些操作需要代理
对普通对象的「读取」概念很宽泛,常见形式与对应拦截器:
| 操作 | 示例 | 拦截器 | 需要的 Reflect 方法 |
|---|---|---|---|
| 访问属性 | obj.foo | get | Reflect.get |
| 判断属性是否存在 | key in obj | has | Reflect.has |
| 遍历对象 | for (const key in obj) {} | ownKeys | Reflect.ownKeys |
| 删除属性 | delete obj.foo | deleteProperty | Reflect.deleteProperty |
| 写属性 | obj.foo = 2 | set | Reflect.set |
二、get:访问属性
const p = new Proxy(obj, {
get(target, key, receiver) {
track(target, key) // 建立联系
return Reflect.get(target, key, receiver) // 返回属性值,receiver 保证 this 正确
}
})
三、has:拦截 in 操作符
in 操作符的运行时逻辑最终会调用对象的内部方法 [[HasProperty]],它对应的拦截函数就是 has:
const p = new Proxy(obj, {
has(target, key) {
track(target, key)
return Reflect.has(target, key)
}
})
effect(() => {
console.log('foo in p', 'foo' in p)
})
四、ownKeys:拦截 for...in
for...in 循环内部会调用抽象方法 EnumerateObjectProperties(obj),其关键点是使用 Reflect.ownKeys(obj) 获取对象自身拥有的键 —— 因此可以用 ownKeys 拦截器拦截。
const ITERATE_KEY = Symbol() // 新增
const p = new Proxy(obj, {
ownKeys(target) {
// 将副作用函数与 ITERATE_KEY 关联
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
}
})
为什么用 ITERATE_KEY:
ownKeys拦截函数只能拿到目标对象target,无法像get/set那样拿到具体的 key;遍历操作不与任何具体的键绑定,所以必须构造唯一的Symbol作为标识。
问题:新增属性不会触发 for...in 的副作用函数
for...in 循环建立的是与 ITERATE_KEY 的联系,而 p.bar = 2 触发的是与 'bar' 相关联的副作用函数,两者无关,因此不会重新执行。
解决思路:在 set 中区分「添加新属性」与「修改已有属性」,新增时同时触发 ITERATE_KEY 关联的副作用函数。
set(target, key, newVal, receiver) {
// 属性不存在 → 添加新属性(ADD),否则是设置已有属性(SET)
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
const res = Reflect.set(target, key, newVal, receiver)
trigger(target, key, type)
return res
}
对应的 trigger:
function trigger(target, key, type) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) effectsToRun.add(effectFn)
})
// 只有 ADD / DELETE 会影响 for...in 的循环结果,需要额外触发 ITERATE_KEY 的依赖
if (type === 'ADD' || type === 'DELETE') {
const iterateEffects = depsMap.get(ITERATE_KEY)
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) effectsToRun.add(effectFn)
})
}
effectsToRun.forEach(effectFn => {
if (effectFn.options.scheduler) effectFn.options.scheduler(effectFn)
else effectFn()
})
}
修改属性值不会改变
for...in的循环次数,所以SET类型不需要触发ITERATE_KEY的副作用函数,否则是多余的性能开销。
五、deleteProperty:拦截删除操作
delete 操作符依赖内部方法 [[Delete]],对应拦截函数 deleteProperty:
deleteProperty(target, key) {
// 检查被操作的属性是否是对象自己的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const res = Reflect.deleteProperty(target, key)
// 只有当属性存在且删除成功时才触发更新
if (res && hadKey) {
trigger(target, key, 'DELETE')
}
return res
}
删除会使对象的键变少、影响
for...in的循环次数,因此'DELETE'与'ADD'一样要触发ITERATE_KEY关联的副作用函数。
相关
- 5.2 JavaScript 对象及 Proxy 的工作原理:拦截器与内部方法的对应关系,是本节所有结论的依据
- 5.4 合理地触发响应:值没有真正变化时不应该触发响应