深层响应的 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?
到目前为止还是没有想到办法。
这样的话,就只有最后一个属性是准确的,前面的就不一定了。
折腾半天,只是知道了一些原理,但是最初的问题还是没有解决。
层次越深,对象结构越复杂,模板里用的越多,这个数据就越长,所以基本没啥用了。
只拿到最后一个属性,没有中间过程的话,对于简单的,或者特定的还是可以用用的,但是想通用就基本没戏了。