Vue3.2 响应式原理解析 (二)

989 阅读14分钟

前言

在上一篇中,简单的看了一下 reactive实现方法,其中有两个文件,专门给reactiveshallowReactivereadonlyshallowReadonly 提供拦截方法,文件分别在:

给数据提供拦截处理方法:vue-next3.2/packages/reactivity/src/baseHandlers.ts

给数组集合提供拦截处理方法:vue-next3.2/packages/reactivity/src/collectionHandlers.ts

这篇文章是我逐行分析完这两个文件所有的理解和收获,和大家分享,记录我学习的历程

工具函数

在此之前,我们先来看几个工具函数,

  • isRef 判断是不是Ref类型
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
  return Boolean(r && r.__v_isRef === true)
}
  • isIntegerKey 是否为数字键
export const isIntegerKey = (key: unknown) =>
  isString(key) &&
  key !== 'NaN' &&
  key[0] !== '-' &&
  '' + parseInt(key, 10) === key
  • isObject 是否为对象
export const isObject = (val: unknown): val is Record<any, any> =>
  val !== null && typeof val === 'object'
  • hasOwn 判断对象中是否有这个key
// 拿到原型上的hasOwnPrototype 进行柯里化
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
  val: object,
  key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)
// 转化成响应式代理对象
const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

// 转换成只读代理对象
const toReadonly = <T extends unknown>(value: T): T =>
isObject(value) ? readonly(value as Record<any, any>) : value

// 不深层次的响应式处理 和直接返回值没啥区别
const toShallow = <T extends unknown>(value: T): T => value

// 拿到方法的原型对象
const getProto = <T extends CollectionTypes>(v: T): any =>
  Reflect.getPrototypeOf(v)

baseHandler

数据读取拦截方法

接受两个参数

isReadonly 只读代理对象?

shallow 浅层次代理?

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // reactive 对 readonly 进行了相关校验 readonly中反之
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      // 代理对象已经存在 返回即可 (有四个集合分别存储不同的代理对象)
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
            ? shallowReactiveMap
            : reactiveMap
        ).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)

    // key 如果是数组方法名称 且是进行过拦截处理的数组原生方法进行操作 arrayInstrumentations
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    // 映射到原始对象上
    const res = Reflect.get(target, key, receiver)

    // 中间做了一些验证 不能是Symbol 不能是特殊属性(__proto__,__v_isRef,__isVue)

    // 如果值是数组、或者是带有数字为键的对象的ref对象,不能展开直接返回
    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }
    
    // 如果是只读就不做依赖收集
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    // 如果获取值不是对象直接返回即可
    // 否则根据isReadonly返回响应式数据
    /* 
       这里做了懒加载处理 到这里之前获取目标的内部数据都不是响应式,这里是对是对象的内部数据的响应式处理 然后返回代理对象
    */
    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拦截方法
const get = /*#__PURE__*/ createGetter() // 可变数据的拦截代理get方法
const shallowGet = /*#__PURE__*/ createGetter(false, true) // 浅层次可变数据的拦截代理get方法
const readonlyGet = /*#__PURE__*/ createGetter(true) // 不可变数据的拦截代理方法
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true) // 浅层次的不可变数据的拦截代理方法

懒加载响应式处理很好的避免了循环引用 vue2中都是用Object.defineproperty一把梭,从头处理到尾,但是如果是半路又引用了新数据,单靠这一个无法做到新数据也进行数据代理,Proxy也是无法对嵌套的数据直接完成数据代理,但是可以在获取数据的时候,对返回的数据进行数据代理处理,下面看数据修改拦截

数据修改拦截方法

创建方法接受一个参数:shallow:浅层次的?

返回拦截方法接受4个参数:

target: 目标数据 key: 键值 value:新的属性值 receivertarget的代理对象

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // 获取旧的属性值
    let oldValue = (target as any)[key]
    // 只有不是浅层次 旧值是ref类型 新值不是 直接在旧值上修改
    if (!shallow) {
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // 在浅层模式下 不管是不是代理对象 都按默认设置
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    // 看看key值的存在于对象中? 
    const hadKey =
      // 是数组 且 key是整数类型
      isArray(target) && isIntegerKey(key)
        // 数组索引不能大于数组的长度
        ? Number(key) < target.length
        // key值存在于存在对象?
        : hasOwn(target, key)
        // 映射的原始对象上
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    // receiver 必须是target 的代理对象 才会触发 trigger
    // Receiver:最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是 proxy 本身)
    if (target === toRaw(receiver)) {
      // 存在旧值 修改 不存在 新增
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

// 通过这个方法生成下面两个set方法
const set = /*#__PURE__*/ createSetter() // 可变数据的拦截set方法
const shallowSet = /*#__PURE__*/ createSetter(true) // 浅层次的可变数据的拦截set方法
// 至于只读的 比较特殊 需要特殊处理

上面有几个点需要详细的解释一下:

  • !isArray(target) && isRef(oldValue) && !isRef(value)

    前面很好理解 后面的 isRef(oldValue) && !isRef(value) 意思是 旧值是ref类型 需要设置的新值不是, 下面这段代码很好的诠释这种情况

    const {ref, reactive, createApp} = vue
    setup() {
         let count = ref(0)
         const state = reactive({
           count
         })
    
         setTimeout(() => {
           state.count = 30
         }, 1000)
    
         return {
           state
         }
       }
    
    
  • 关于 target === toRaw(receiver) 这个点的含义

    可以看看 MDN上的 Proxy 的解释,在以前,我只是认为 receivertarget 的代理对象 转换原始对象之后应该会和 target 绝对相等,在我阅读了一些文章之后,终于找到了详细解答 链接地址:juejin.cn/post/684490…

  • 如果使用数组原生方法去改变数组,那必然会被会触发两次set 甚至于无限调用,所以vue3.2对数组的5个改变数组本身的方法进行劫持

function createArrayInstrumentations() {
  const instrumentations: Record<string, Function> = {}
  // instrument identity-sensitive Array methods to account for possible reactive
  // values
  // 3个判断数组中是否存在某值的方法
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
    instrumentations[key] = function(this: unknown[], ...args: unknown[]) {
      const arr = toRaw(this) as any
      // 收集依赖
      for (let i = 0, l = this.length; i < l; i++) {
        track(arr, TrackOpTypes.GET, i + '')
      }
      // we run the method using the original args first (which may be reactive)
      // 使用传递进来的参数第一次运行方法 (参数可能是代理对象, 会找不到结果) 找到了结果 返回即可
      const res = arr[key](...args)
      if (res === -1 || res === false) {
        // 将代理对象转换成原始数据 并再一次运行 且返回
        // if that didn't work, run it again using raw values.
        return arr[key](...args.map(toRaw))
      } else {
        return res
      }
    }
  }) 
  // instrument length-altering mutation methods to avoid length being tracked
  // which leads to infinite loops in some cases (#2137)
  // 5个会修改数组本身的方法
  ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
    instrumentations[key] = function(this: unknown[], ...args: unknown[]) {
      // 在vue3.0版本 vue会对数组的push等方法进行依赖收集和触发 可能产生无限循环调用 这里让数组的push等方法不进行依赖的收集和触发
      /**
       * watachEffect(() => {
       *  arr.push(1)
       * })
       * 
       * watchEffect(() => {
       *  arr.push(2)
       * })
       */
      pauseTracking()
      // 执行数组原生上的方法 将结果返回
      const res = (toRaw(this) as any)[key].apply(this, args)
      resetTracking()
      return res
    }
  })
  return instrumentations
}

也就是说在调用原生方法改变数组时,不会再去收集依赖和触发以来进行更新 而是同意调用 每一个组件唯一的挂载 更新函数

其他的拦截方法

// 拦截删除数据
function deleteProperty(target: object, key: string | symbol): boolean {
// 判断key键是否存在 然后删除操作
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  // 只有key键存在 删除成功了才会进行更新
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

// 判断是否存在
function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  // isSymbol不是唯一值 builtInSymbols 不是Symbol原型上的12个方法
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}

// 拿到自身所有属性组成的数组
function ownKeys(target: object): (string | symbol)[] {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

总结

Vue3的响应式是通过代理对象Proxy实现的,任何操作都会通过Reflect进行映射到原始对象,获取的操作一般都是收集依赖,修改或者是新增、删除是触发依赖,对于数组的操作拦截,Proxy并不能很好的处理完美,需要对数组的方法进行劫持,所有的拦截操作都封装进了createReactiveObject 函数中

collectionHandler

vue3对数组集合类型专门写拦截方法,当点开collectionHandlers.ts时

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}

export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, true)
}

export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(true, false)
}

export const shallowReadonlyCollectionHandlers: ProxyHandler<
  CollectionTypes
> = {
  get: /*#__PURE__*/ createInstrumentationGetter(true, true)
}

会发现vue3只给定义了一个get,那vue3是如何拦截其他操作的?其实这是因为数组集合类型和其他的数据类型有着不同的操作方式:使用自己独有的API进行操作,比如 add、get、等方法,这些操作都被统一的封装进了 createInstrumentations 函数中

为什么要重写

这Set和Map内部的实现原理有关,Set和Map内部数据都是通过this去访问的,被称为内存插槽,在直接通过接口去访问的时候,this指向的是Set, 通过代理对象去访问时,this指向就变成了proxy,也就无法访问。详细解释

集合读操作

function get(
  target: MapTypes,
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  // #1772: readonly(reactive(Map)) should return readonly + reactive version
  // of the value
  // target可能是:只读代理对象 原始数据可能是一个可变代理对象 
  // 需要通过Reactive.Flags.RAW拿到只读代理对象原始数据(或许是可变代理对象) 之后在用toRaw获取一次
  target = (target as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  // 由于Map可以使用对象作为key 有可能会有代理对象作为key 这里拿到原始key
  const rawKey = toRaw(key)
  // 无论是否 key 和 rawKey是否相同 都去收集依赖
  if (key !== rawKey) {
    !isReadonly && track(rawTarget, TrackOpTypes.GET, key)
  }
  !isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
  // 原始数据原型上的has方法
  const { has } = getProto(rawTarget)
  // 根据调用的响应式api的不同找到拿到不同的方法
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  if (has.call(rawTarget, key)) {
    // key对应的值存在
    return wrap(target.get(key))
  } else if (has.call(rawTarget, rawKey)) {
    // rawKey 值存在
    return wrap(target.get(rawKey))
  } else if (target !== rawTarget) {
    // #3602 readonly(reactive(Map))
    // ensure that the nested reactive `Map` can do tracking for itself
    // key 和 rawKey都不存在 且两个数据不一样(这样代表target是一个可变代理对象) 只能代理对象自己去追踪
    target.get(key)
  }
}

这个方法有个地方需要注意

如果使用reactive和readonly嵌套使用 readonly(reactive(Map)),(在之前的版本没有这样处理,会导致Map的响应效果消失 请查看这个 issues) 所以target可能是一个代理对象,rawTarget也是Map对象,索性让代理他自己去追踪,

在调用这get方法的时候,第一个入参的this不能和Proxy的get方法的第一个参数弄混,这个this指的是已经代理过的代理对象,拦截方法的第一个参数一般都是target 原始对象,内部的this一般都是代理对象本身

集合写操作

需要拦截的操作有两种,分别对应着Map和Set的set和add方法,都做了不同的处理

  • Set、WeakSet写操作
// Set WeakSet 独有
function add(this: SetTypes, value: unknown) {
  // value 可能是 代理对象 拿到原始value
  value = toRaw(value)
  // target 是一个代理对象, 需要拿到原始数据
  const target = toRaw(this)
  // 获得原型,并使用has方法判断value 是否存在于target中
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  // 不存在 则添加值,并且触发依赖 
  if (!hadKey) {
    target.add(value)
    trigger(target, TriggerOpTypes.ADD, value, value)
  }
  // 返回自己
  return this
}
  • Map、WeakMap写操作
// Map WeakMap独有
function set(this: MapTypes, key: unknown, value: unknown) {
  // value 可能是 代理对象 拿到原始value
  value = toRaw(value)
  // target 是一个代理对象, 需要拿到原始数据
  const target = toRaw(this)
  // 原型上的 has、get方法
  const { has, get } = getProto(target)

  // 判断值是否存在,后面用来判断是修改还是新增
  let hadKey = has.call(target, key)
  if (!hadKey) {
    // 不存在 获取原始key 重新在获取一次
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    // 存在 但是为了防止key和rawKey都存在与target 获取不准确 进行校验
    checkIdentityKeys(target, has, key)
  }

  // 获取旧值
  const oldValue = get.call(target, key)
  target.set(key, value)
  // 存在是修改 不存在是新增 之后触发依赖
  if (!hadKey) {
    trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue)) {
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  return this
}

写操作相对简单一些,最主要的就是判断原本是否存在旧值,触发的依赖不同,Set和WeakSet是存在是修改、不存在是新增,而Map是存在了就不会去依赖,而且相对于baseHandler使用Reflect映射的原始对象上,而这里是用本身的API去操作,

集合迭代器

function createIterableMethod(
  method: string | symbol,
  isReadonly: boolean,
  isShallow: boolean
) {
  return function(
    this: IterableCollections,
    ...args: unknown[]
  ): Iterable & Iterator {
    // target可能是:只读代理对象 原始数据可能是一个可变代理对象 
    // 需要通过Reactive.Flags.RAW拿到只读代理对象原始数据(或许是可变代理对象)
    const target = (this as any)[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    // 目标是Map类型吗
    const targetIsMap = isMap(rawTarget)
    // Set是没有entries方法,这是Map的迭代方法  只有Map去调用,isPair为true
    // 每一次迭代返回的结构是 [key value] 形式的数组
    // 后面的(method === Symbol.iterator && targetIsMap) 是因为Symbol.iterator调用触发的entries
    const isPair =
      method === 'entries' || (method === Symbol.iterator && targetIsMap)
    // Set是没有keys方法的,这是Map的迭代方法,
    const isKeyOnly = method === 'keys' && targetIsMap
    // 执行原生的迭代方法 keys values entries
    const innerIterator = target[method](...args)
    // 根据条件 warp 返回对应的方法 例如 如果是 reactive 返回的就是 toReactive
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    // 不是只读 收集依赖
    !isReadonly &&
      track(
        rawTarget,
        TrackOpTypes.ITERATE,
        isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
      )
    // return a wrapped iterator which returns observed versions of the
    // values emitted from the real iterator
    // 返回一个包转过的迭代器 这个迭代器的返回值都是由默认迭代器返回
    return {
      // iterator protocol
      next() {
        // 去除重要的两个值 当前迭代的值、是否迭代完成
        const { value, done } = innerIterator.next()
        // 如果done最后是true 代表迭代到最后,返回的值是undefined 没有必要做响应式了
        return done
          ? { value, done }
          : {
            // 如果是有一对 返回的数组中 索引0是key 索引1是value 不是代表是Set和WeakSet 就只有值
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
              done
            }
      },
      // iterable protocol 返回迭代对象本身
      // 自定义迭代器 返回自己迭代对象自己本身
      [Symbol.iterator]() {
        return this
      }
    }
  }
}

在迭代方面上,Map和Set有所不同,

1637055950(1).jpg

1637055994(1).jpg

上面两张图片分别是Map和Set的原型,一共有三个地方不同

1.Set的entrieskeys 调用的其实是values方法,而Map是有这两个单独的方法

2.Map的Symbol(symbol.iterator)调用的是entries 而 Set的Symbol(symbol.iterator)调用的还是values()

  1. 两种数据的添加方式不同,Map使用set方法可以设置key值,Set使用add方法不可以设置key值

正因为这三种不同,才有了上面不同的处理,最主要的逻辑就是包装了迭代器,并且next返回的值每次都对应的响应式api进行处理,

迭代还有一个方法

function createForEach(isReadonly: boolean, isShallow: boolean) {
  return function forEach(
    this: IterableCollections,
    callback: Function,
    thisArg?: unknown
  ) {
    // 存储代理对象 拿到代理之前的数据 拿到原始数据
    const observed = this as any
    const target = observed[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    // 根据条件 warp 返回对应的方法 例如 如果是 reactive 返回的就是 toReactive
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
    return target.forEach((value: unknown, key: unknown) => {
      // important: make sure the callback is
      // 1. invoked with the reactive map as `this` and 3rd arg
      // 2. the value received should be a corresponding reactive/readonly.
      // 为了更好的遍历 由内部调用外界进来函数,并且数据是只读或者是响应式的
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
}

逻辑上并不复杂,最主要的是劫持了方法forEach,并将数据进行代理,

总结

由于数组集合的底层设计的原因,无法通过Proxy进行劫持,只能通过劫持自身接口进行代理,可能反射带劫持方法上,也可能通过映射到原始对象上,

劫持方法,会先拿到原始对象和传递进来的原始数据,再原始对象的原型上的方法,把this绑定为原始对象进行调用。

对于get和has,插入收集依赖的逻辑,然后再将返回值进行转换(因为has返回的是布尔值,不要转换),迭代器也是如此,但是最后需要把迭代过程的数据转换成响应式返回

写操作需要,需要插入触发依赖进行更新的逻辑

最后总结

到这里baseHandler和collectionHandler的逻辑就全部解析完了,也查阅了很多资料,收获了很多底层知识,写了老半天,希望各位大佬能够补充,谢谢