前面我们介绍了reactive处理对象的一些边界问题,这篇继续来处理剩余的问题:
代理对象作为原型的问题:
const obj = {}
const proto = { bar: 1 }
const parent = reactive(proto)
const child = reactive(obj)
// 把parent设为child的原型
Object.setPrototypeOf(child, parent)
effect(() => {
console.log(child.bar)
})
// 修改bar,会触发副作用函数执行两次
child.bar = 2
在这段代码中我们使用上一篇文章中的reactive创建了响应式数据child和parent,并把parent设为child的原型,然后在副作用函数中读取child.bar,这时候会触发child的get"陷阱函数"拦截访问,在拦截函数内使用上篇文章介绍过的Refelct.get(target,key,receiver)获取最终结果。但是child上并没有bar,因此沿着原型链往上找找到parent中的bar,虽然可以找到,但是对于parent.bar的访问又会触发parent的get"陷阱函数"拦截访问,和副作用函数产生响应联系。
如果这时候修改child.bar,JS依然会按照相同的逻辑,最终在parent上找到bar,这个过程依然会依次触发child和parent的set“陷阱函数”执行trigger。所以最终副作用函数会被执行两次
要想解决这个问题也很容易,我们知道get和set中的receiver是指向真正访问值的对象,我们在访问bar的时候是使用child,所以receiver也就是child,而这时候的target是obj。也就是说,这时候的receiver就正好是target的代理对象。但是如果是从parent中访问bar,receiver依然是child,target是proto,不满足receiver是target的代理对象这一条件了,所以我们就从这里入手,屏蔽掉parent触发的这一次副作用函数:
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 给拦截函数添加一个能力,如果key是raw,就返回原始数据
if (key === 'raw') {
return target
}
// 省略部分代码
},
set(target, key, newVal, receiver) {
// 省略部分代码
if (target === receiver.raw) {
trigger(target, key, type)
}
// 省略部分代码
}
})
}
我们给get拦截函数(为了方便我称它为拦截函数,就不再称其为陷阱函数了(英文为trap))添加了一个能力,访问代理对象的raw属性时返回它的原始值target。
我们从child中访问bar所以child就是receiver,所以这样就可以判断child是不是obj的代理对象了,如果是的话就trigger,如果不是就什么都不做。
深响应和浅响应:
我们之前讨论的通过代理实现响应式都是浅响应的,如果存在对象嵌套也只是单纯返回值,并不会进一步做代理,所以我们还要判断get拦截函数的返回值是不是对象,如果是,那么调用reactive对它进行响应式转换,然后才返回:
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 给拦截函数添加一个能力,如果key是raw,就返回原始数据
if (key === 'raw') {
return target
}
const res = Reflect.get(target, key, receiver)
// 如果返回值是对象,那么继续包装为响应式数据并返回
if (typeof res === "object" && res !== null) {
return reactive(res)
}
return res
},
})
}
但是Vue3还提供了显式设置浅响应的API,也就是shallowReactive,实现也很简单,我们封装一个createReactive函数:
function createReactive(obj, isShallow = false) {
return new Proxy(obj, {
get(target, key, receiver) {
// 给拦截函数添加一个能力,如果key是raw,就返回原始数据
if (key === 'raw') {
return target
}
const res = Reflect.get(target, key, receiver)
// 如果浅响应那么直接返回res
if (isShallow) {
return res
}
if (typeof res === 'object' && res !== null) {
return reactive(res)
}
return res
// 省略部分代码
}
})
}
通过isShallow来决定到底这个响应式数据是不是浅响应,默认行为是深响应,所以这样就可以实现reactive和shallowReactive():
// 深度响应
function reactive(obj) {
return createReactive(obj)
}
// 浅响应
function shallowReactive(obj) {
return createReactive(obj, true)
}
总结一下:这篇文章讲的两个问题一个是代理对象作为原型的问题,一个是深浅响应的问题。第一个问题虽然理解起来可能稍微有点难度,但是比较偏细节,原理就是判断target和receiver之间的关系,只有receiver是target的代理对象时才执行trigger,避免了两次执行trigger的问题。第二个问题深浅响应,我们知道Vue2为了实现data的响应式,为data递归的调用defineProperty,递归得把data的属性转为getter和setter,Vue3中放弃了这种在定义数据时就把响应式数据处理好的方式,改为如果需要的是响应式数据,那么就返回响应式API包裹后的值。从而提升了性能