5.8.3 避免污染原始数据
Map的读写都要走自定义方法;把响应式数据写回原始数据会造成「数据污染」。
一、Map 的 get / set
const mutableInstrumentations = {
// 拦截 Map 的 get 方法
get(key) {
const target = this.raw
const had = target.has(key) // 判断 key 是否存在
track(target, key) // 追踪依赖,建立响应联系
if (had) {
const res = target.get(key)
// 结果仍是可代理的数据时,包装成响应式数据返回(深响应)
return typeof res === 'object' ? reactive(res) : res
}
},
// 拦截 Map 的 set 方法
set(key, value) {
const target = this.raw
const had = target.has(key)
const oldValue = target.get(key) // 先取旧值
const res = target.set(key, value)
if (!had) {
trigger(target, key, 'ADD', value) // 不存在 → 新增
} else if (oldValue !== value || (oldValue === oldValue && value === value)) {
trigger(target, key, 'SET', value) // 存在且值变了 → 修改
}
return res
}
}
与普通对象的
set一致,Map 也需要区分ADD/SET,并处理NaN的情况。
二、什么是「数据污染」
const m = new Map()
const p1 = reactive(m)
const p2 = reactive(new Map())
// 为 p1 设置一个键值对,值是代理对象 p2
p1.set('p2', p2)
effect(() => {
// 通过原始数据 m 读取数据值
console.log(m.get('p2').size)
})
// 通过原始数据 m 设置数据值,副作用函数竟然重新执行了
m.get('p2').set('foo', 1)
原因:set 方法内把 value 原样设置到了原始数据 target 上。当 value 是响应式数据时,原始对象里存的也是响应式数据 —— 这就是数据污染。
危害:原始数据不应具有响应式能力,否则用户既能操作原始数据又能操作响应式数据,代码会「乱套」。
三、解决:写入前取原始值
set(key, value) {
const target = this.raw
const had = target.has(key)
const oldValue = target.get(key)
// 如果 value 是响应式数据,通过 raw 拿到原始数据再写
const rawValue = value.raw || value
const res = target.set(key, rawValue)
// ...ADD / SET 判断与 trigger
return res
}
易错点:一直用
raw属性访问原始数据是有缺陷的 —— 它可能与用户自定义的raw属性冲突。严谨的实现应使用唯一的Symbol值作为访问原始数据的键。
相关
- 5.8.2 建立响应联系:
add/delete的自定义实现 - 5.8.4 处理forEach:集合的遍历方法与
SET操作引发的响应