代码改变世界

reactive是如何实现深层响应的?

2021-04-17 12:13  金色海洋(jyk)  阅读(51)  评论(0编辑  收藏  举报

深层响应的 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?

到目前为止还是没有想到办法。

这样的话,就只有最后一个属性是准确的,前面的就不一定了。

折腾半天,只是知道了一些原理,但是最初的问题还是没有解决。

监听结果

层次越深,对象结构越复杂,模板里用的越多,这个数据就越长,所以基本没啥用了。

只拿到最后一个属性,没有中间过程的话,对于简单的,或者特定的还是可以用用的,但是想通用就基本没戏了。

2