5.4 合理地触发响应
触发响应要「合理」:值没变不该触发,
NaN不能被判成变了,原型链上的继承属性不能触发两次。
一、新值等于旧值时不应触发
在 set 拦截函数内先取出旧值 oldVal,只有新旧值不全等时才触发响应:
set(target, key, newVal, receiver) {
const oldVal = target[key] // 先获取旧值
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
const res = Reflect.set(target, key, newVal, receiver)
if (oldVal !== newVal) { // 值真的变了才触发
trigger(target, key, type)
}
return res
}
二、NaN 的处理
NaN === NaN 恒为 false,若只判断 oldVal !== newVal,把 NaN 重新赋成 NaN 也会误触发。因此再加一个条件:不全等的同时,保证两者不都是 NaN。
// oldVal === oldVal 为 false 说明 oldVal 是 NaN;newVal 同理
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
| 旧值 | 新值 | oldVal !== newVal | 附加条件 | 是否触发 |
|---|---|---|---|---|
| 1 | 1 | false | — | 否 |
| 1 | 2 | true | true | 是 |
| NaN | NaN | true | false | 否 |
| NaN | 2 | true | true | 是 |
三、继承属性导致的重复触发
复现
const obj = {}
const proto = { bar: 1 }
const child = reactive(obj)
const parent = reactive(proto)
Object.setPrototypeOf(child, parent) // parent 作为 child 的原型
effect(() => {
console.log(child.bar)
})
child.bar = 2 // 副作用函数执行了两次
成因
- 按 [[Get]] 的执行流程:对象自身没有该属性时会去获取原型并调用原型的
[[Get]];child.bar实际取的是parent.bar,而parent也是响应式数据,于是副作用函数同时被child.bar与parent.bar收集。 - 按 [[Set]] 的执行流程:设置的属性不存在于对象自身时,会取得原型并调用原型的
[[Set]]; 于是child与parent两个代理对象的set拦截函数都会执行,各触发一次响应 → 副作用函数执行两次。
解决:比较 receiver 与 target
拦截函数中的关键观察:
- 最初设置的是
child.bar,所以无论走到哪一层,receiver始终是child,而变化的是target; - 因此只有
receiver就是target的代理对象时才触发更新,就能屏蔽由原型引起的更新。
为此给 reactive 增加一个 raw 属性,用于从代理对象取回原始数据:
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 代理对象通过 raw 属性访问原始数据
if (key === 'raw') return target
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, newVal, receiver) {
const oldVal = target[key]
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
const res = Reflect.set(target, key, newVal, receiver)
// target === receiver.raw 说明 receiver 就是 target 的代理对象
if (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
}
return res
},
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
ownKeys(target) {
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
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
}
})
}
reactive只是对Proxy做了一层封装,raw这种「特殊 key」的约定在后面的只读、浅响应中也会复用。
相关
- 5.3 如何代理 Object:set/trigger 的类型区分从何而来
- 5.5 浅响应与深响应:只用
reactive还不够,嵌套对象如何做到深响应