深层响应的 reactive
看过官网文档的都知道,Vue3 的响应性分为浅层和深层,我们常用的 reactive 是深层的。
我们也都知道,reactive 是使用 proxy 来实现响应性的,那么问题来了:
既然 proxy 的拦截操作是浅层的,对于嵌套属性的操作无感,那么 reactive 是如何实现深层响应的呢?
这个就得看看 源码了。
// reactivity.js
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
if (key === "__v_isReactive" /* IS_REACTIVE */) {
return !isReadonly;
}
else if (key === "__v_isReadonly" /* IS_READONLY */) {
return isReadonly;
}
else if (key === "__v_raw" /* RAW */ &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap).get(target)) {
return target;
}
const targetIsArray = isArray(target);
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
const res = Reflect.get(target, key, receiver);
if (isSymbol(key)
? builtInSymbols.has(key)
: isNonTrackableKeys(key)) {
return res;
}
if (!isReadonly) {
track(target, "get" /* GET */, key);
}
if (shallow) {
return res;
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key);
return shouldUnwrap ? res.value : res;
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res); // 重点在这里。。。
}
return res;
};
}
这是拦截 get 操作的代码。
上面的可以跳过,直接看倒数第二个 return。
简单地说,各种判断后,返回一个新的 reactive。
就是说,给子子属性赋值的时候,需要先获取第一级的对象,然后把这个对象变成 reactive 的形式返回,这样就可以实现层层属性的拦截了。
监听任意属性的值的变化。
最简单的方式就是用 watch 的深度监听功能。
watch (() => reactive1, () => {
// 属性值变了。
}, {deep:true})
这样任意一层的属性的变化,都可以获知,只是有个小问题,只知道有属性值变了,但是不知道具体是哪个属性变了。两个参数也都是新值,没有旧值了。
那么如果一定要知道是哪个属性变了呢?
用 proxy 套个娃
既然 Proxy 里面可以进行各种拦截,那么为啥不顺便返回来改了哪个属性呢?
不管那么多了,自己给 reactive 套个 proxy 再次拦截试一试。
const myProxy = (_target, callback, arr) => {
const _arr = arr || []
const proxy = new Proxy(_target, {
get: function (target, key, receiver) {
switch (key) {
case '__v_isRef':
case 'toJSON':
case 'symbol':
case 'Symbol(Symbol.toStringTag)':
break;
default:
// 判断是不是对象
if (typeof target[key] === 'object') {
// console.log(`获取 对象 ${key}!`, target[key])
_arr.push(key)
// 源头监听
if (typeof callback === 'function') {
callback('get', key, target[key], _arr)
}
} else if (typeof key !== 'symbol') {
// console.log('获取 属性 ', key, target[key])
}
break;
}
// 调用原型方法
const res = Reflect.get(target, key, target)
if (typeof res === 'object') {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return myProxy(res, callback, _arr) // 递归
}
return res
},
set: function (target, key, value, receiver) {
if (key !== '__watch') {
// 源头监听
if (typeof callback === 'function') {
callback('set', key, value, _arr)
}
// console.log('路径:', _arr.join('-'))
_arr.length = 0
// console.log(`设置 ${key}:${value}!`)
}
// 调用原型方法
return Reflect.set(target, key, value, target)
}
})
// 返回实例
return proxy
}
使用方式
const ret3 = myProxy({
a:'11',
b: {
b1:'',
b2: {
b21: {
b211: '111'
}
},
b3: {
b31: {
b311: '2222'
}
}
}
}, (kind, key, value, path) => {
console.log(`ret3 - 定义端监听:【${kind}】 ${key}-`, value, path)
})
const retChage = () => {
ret3.b.b2.b21.b211 = 'eeee'
}
-
callback
古老的回调函数,把属性名称和属性值返回来就好。 -
_arr
因为嵌套属性可能是很多级别的,而 set 只能获知最后一个属性的名称,中间的过程全在 get 里面。
于是就想做个数组把每一级的属性名称存进去。
修改属性的时候也确实是一级一级的存进去了,但是直到我把 ret3 放到了模板里面……
模板里面也是要获取值的,也会触发 get 事件,也会往数组里面 push 属性名称。
于是问题来了,如何区分是模板触发的 get 还是给属性赋值触发的 get?
到目前为止还是没有想到办法。
这样的话,就只有最后一个属性是准确的,前面的就不一定了。
折腾半天,只是知道了一些原理,但是最初的问题还是没有解决。
层次越深,对象结构越复杂,模板里用的越多,这个数据就越长,所以基本没啥用了。
只拿到最后一个属性,没有中间过程的话,对于简单的,或者特定的还是可以用用的,但是想通用就基本没戏了。