vue3的响应式设计——proxy

概要

vue3的 reactivity 是一个独立的包,这是一个比较大的改动,所有响应式相关的实现都在里面,我主要讲的也就是这一块的。

知识准备

1.proxy: es6的代理实现方式
2.reflect: 将object对象一些明显属于语言内部方法,放到Reflect上,
3.weakMap: WeakMap 的 key 只能是 Object 类型。
4.weakSet: WeakSet 对象是一些对象值的集合, 并且其中的每个对象值都只能出现一次.
响应式简要实现
我们曾经的书写响应式数据是这样的

data () {
    return {
        count: 0
    }
}

然后vue3新的响应式书写方式(老的也兼容)

 setup() {
     const state = {
         count: 0,
         double: computed(() => state.count * 2)
     }
     function increment() {
         state.count++
     }

    onMounted(() => {
        console.log(state.count)
    })

    watch(() => {
        document.title = `count ${state.count}`
   })
    return {
        state,
      increment
   }
}

感觉setup这块就有点像 react hooks 理解成一个带有数据的逻辑复用模块,不再以vue组件为单位的代码复用了
和React钩子不同,setup()函数仅被调用一次。
所以新的响应书数据两种声明方式:
1.Ref
前提:声明一个类型 Ref

export interface Ref<T> {
  [refSymbol]: true
  value: UnwrapNestedRefs<T>
}

ref()函数源码:

 function ref(raw: unknown) {
   if (isRef(raw)) {
     return raw
   }
   // convert 内容:判断 raw是不是对象,是的话 调用reactive把raw响应化
   raw = convert(raw)
   const r = {
     _isRef: true,
     get value() {
      // track 理解为依赖收集
      track(r, OperationTypes.GET, '')
      return raw
    },
    set value(newVal) {
      raw = convert(newVal)
      // trigger 理解为触发监听,就是触发页面更新好了
      trigger(r, OperationTypes.SET, '')
    }
  }
  return r as Ref
}

还是看下 convert 吧

const convert = val => isObject(val) ? reactive(val) : val

可以看得出 ref类型 只会包装最外面一层,内部的对象最终还是调用reactive,生成Proxy对象进行响应式代理。
疑问
可能有人想问,为什么不都用proxy, 内部对象都用proxy,最外层还要搞个 Ref类型,多此一举吗?
理由可能比较简单,那就是proxy代理的都是对象,对于基本数据类型,函数传递或对象结构是,会丢失原始数据的引用。
官方解释:

However, the problem with going reactive-only is that the consumer of a composition function must keep the reference to the returned object at all times in order to retain reactivity. The object cannot be destructured or spread:

2.Reactive
前提:先了解下 weakMap

// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>() // key:原始对象 value: Proxy
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
reactive(target)

源码如下:
注:target一定是一个对象,不然会报警告

 function reactive(target) {
   // 如果target是一个只读响应式数据
   if (readonlyToRaw.has(target)) {
     return target
   }
   // 如果是被用户标记的只读数据,那通过readonly函数去封装
   if (readonlyValues.has(target)) {
     return readonly(target)
   }
  // go ----> step2
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers, // 注意传递
    mutableCollectionHandlers
  )
}

createReactiveObject(target,toProxy,toRaw,baseHandlers,collectionHandlers)

 function createReactiveObject(
   target: unknown,
   toProxy: WeakMap<any, any>,
   toRaw: WeakMap<any, any>,
   baseHandlers: ProxyHandler<any>,
   collectionHandlers: ProxyHandler<any>
 ) {
     // 判断target不是对象就 警告 并退出
   if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // 通过原始数据 -> 响应数据的映射,获取响应数据
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // 如果原始数据本身就是个响应数据了,直接返回自身
  if (toRaw.has(target)) {
    return target
  }
  // 如果是不可观察的对象,则直接返回原对象
  if (!canObserve(target)) {
    return target
  }
  // 集合数据与(对象/数组) 两种数据的代理处理方式不同
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  // 声明一个代理对象 ----> step3
  observed = new Proxy(target, handlers)
  // 两个weakMap 存target observed
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

baseHandles
(我们以对象类型为例,集合类型的handlers稍复杂点)
handlers如下,new Proxy(target, handles)的 handles就是下面这个对象

export const mutableHandlers = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}

createGetter(false)
问题:如何代理多层嵌套的对象
关键词:利用 proxy 的 get
思路:当我们代理get获取到res时,判断res 是否是对象,如果是那么 继续reactive(res),可以说是一个递归

reactive(target) ->
createReactiveObject(target,handlers) ->
new Proxy(target, handlers) ->
createGetter(readonly) ->
get() -> res ->
isObject(res) ? reactive(res) : res

 function createGetter(isReadonly: boolean) {
   // isReadonly 用来区分是否是只读响应式数据
   // receiver即是被创建出来的代理对象
   return function get(target: object, key: string | symbol, receiver: object) {
     // 获取原始数据的响应值
     const res = Reflect.get(target, key, receiver)
     if (isSymbol(key) && builtInSymbols.has(key)) {
       return res
     }
    if (isRef(res)) {
      return res.value
    }
    // 收集依赖
    track(target, OperationTypes.GET, key)
    // 这里判断上面获取的res 是否是对象,如果是对象 则调用reactive并且传递的是获取到的res,
    // 则形成了递归
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}

set
set的一个主要作用去触发监听,使试图更新,需要注意的是控制什么时候才是视图需要真的更新

 function set(
   target: object,
   key: string | symbol,
   value: unknown,
   receiver: object
 ): boolean {
   // 拿到新值的原始数据
   value = toRaw(value)
   // 获取旧值
  const oldValue = (target as any)[key]
  // 如果旧值是Ref类型,新值不是,那么直接更新值,并返回
  if (isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
  const hadKey = hasOwn(target, key)
  const result = Reflect.set(target, key, value, receiver)
  // 如果是原始数据原型链上的数据操作,不做任何触发监听函数的行为。
  if (target === toRaw(receiver)) {
    // 更新的两种条件 
    // 1. 不存在key,即当前操作是在新增属性
    // 2. 旧值和新值不等
    if (!hadKey) {
      trigger(target, OperationTypes.ADD, key)
    } else if (hasChanged(value, oldValue)) {
      trigger(target, OperationTypes.SET, key)
    }
  }
  return result
}

问题2:
对于数据的set操作会出发多次traps,
这里有个前提了解:就是我们日常修改数组,比如 let a = [1], a.push(2),
这个push操作,我们是实际上是对a做了2个属性的修改,1,set length 1; 2. set value 2
所以我们的set traps会出发多次
思路:通过属性值和value控制,比如当 set key是 length的时候,我们可以判断当前数组 已经有此属性,所以不需要出发更新,当新设置的值和老值一样是也不需要更新(说辞不够严谨)

问题3:
set的源码里面有 有一个 target === toRaw(receiver)条件下才继续操作 trigger更新视图
这里就暴露出一个东西,即存在 target !== toRaw(receiver)
Receiver: 最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是 proxy 本身)
其实源码有注释

// don’t trigger if target is something up in the prototype chain of original

即如果我们的操作是操作原始数据原型链上的数据操作,target 就不等于 toRaw(receiver)
什么情况下 target !== toRaw(receiver)
例如:

 const child = new Proxy(
   {},
   { // 其他 traps 省略
     set(target, key, value, receiver) {
       Reflect.set(target, key, value, receiver)
       console.log('child', receiver)
       return true
     }
   }
)

const parent = new Proxy(
  { a: 10 },
  { // 其他 traps 省略
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver)
      console.log('parent', receiver)
      return true
    }
  }
)

Object.setPrototypeOf(child, parent) // child.__proto__ === parent true

child.a = 4

// 结果
// parent Proxy {a: 4}
// Proxy {a: 4}

从结果可以看出,理论上 parent的set应该不会触发,但实际是触发了,此时

target: {a: 10}
receiver: Proxy {a: 4}
// 在vue3中
toRaw(receiver): {a: 4} 

为什么有了proxy做响应式还需要一个Ref呢?
因为Proxy无法劫持基础数据类型,所以设计了这么一个对象——Ref,其实还是有很多设计细节,就不一一赘述了,官网也给了他们不同点,可以自己去好好了解。

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注