vue3的响应式设计——proxy

概要

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

知识准备

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

data () {
    return {
        count: 0
    }
}复制代码

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

数组

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}`
复制代码
复制代码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()函数源码:ui

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 吧spa

const convert = val => isObject(val) ? reactive(val) : val复制代码

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

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,其实仍是有不少设计细节,就不一一赘述了,官网也给了他们不一样点,能够本身去好好了解。

相关文章
相关标签/搜索