美文网首页【vue3源码】
【vue3源码】八、reactive——Collection的响

【vue3源码】八、reactive——Collection的响

作者: MAXLZ | 来源:发表于2022-08-17 17:45 被阅读0次

    【vue3源码】八、reactive——Collection的响应式实现

    参考代码版本:vue 3.2.37

    官方文档:https://vuejs.org/

    前文中我们分析了reactiveObject类型的数据处理,这篇文章继续介绍对集合的处理。

    mutableCollectionHandlers

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

    对于集合,读取操作和修改操作都是通过调用方法(size除外)进行,所以只需要捕获其get方法即可。get捕获器通过createInstrumentationGetter函数生成。

    function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
      const instrumentations = shallow
        ? isReadonly
          ? shallowReadonlyInstrumentations
          : shallowInstrumentations
        : isReadonly
        ? readonlyInstrumentations
        : mutableInstrumentations
    
      return (
        target: CollectionTypes,
        key: string | symbol,
        receiver: CollectionTypes
      ) => {
        // 处理特殊的key
        if (key === ReactiveFlags.IS_REACTIVE) {
          return !isReadonly
        } else if (key === ReactiveFlags.IS_READONLY) {
          return isReadonly
        } else if (key === ReactiveFlags.RAW) {
          return target
        }
    
        return Reflect.get(
          hasOwn(instrumentations, key) && key in target
            ? instrumentations
            : target,
          key,
          receiver
        )
      }
    }
    

    createInstrumentationGetter函数接收两个参数:isReadonly(是否是只读响应式)、shallow(是否是浅层响应式)。

    首先根据isReadonlyshallow的值,获取对应的instrumentations

    const instrumentations = shallow
      ? isReadonly
        ? shallowReadonlyInstrumentations
        : shallowInstrumentations
      : isReadonly
      ? readonlyInstrumentations
      : mutableInstrumentations
    

    instrumentations是什么呢?

    instrumentations就是个对象,它通过createInstrumentations生成,内部重写了集合的一些方法。createInstrumentations方法创建四个instrumentations

    • mutableInstrumentations:处理可修改的响应式集合数据
    • readonlyInstrumentations:处理只读的响应式集合数据
    • shallowInstrumentations:处理浅层响应式集合数据
    • shallowReadonlyInstrumentations:处理浅层只读响应式集合数据
    const [
      mutableInstrumentations,
      readonlyInstrumentations,
      shallowInstrumentations,
      shallowReadonlyInstrumentations
    ] = /* #__PURE__*/ createInstrumentations()
    

    mutableInstrumentations

    const mutableInstrumentations: Record<string, Function> = {
      get(this: MapTypes, key: unknown) {
        return get(this, key)
      },
      get size() {
        return size(this as unknown as IterableCollections)
      },
      has,
      add,
      set,
      delete: deleteEntry,
      clear,
      forEach: createForEach(false, false)
    }
    

    get

    get函数可接收四个参数:target目标集合、keyisReadonly是否是只读响应式、isShallow是否是浅层响应式。

    function get(
      target: MapTypes,
      key: unknown,
      isReadonly = false,
      isShallow = false
    ) {
      // readonly(reactive(Map)) 应该返回值的readonly + reactive
      target = (target as any)[ReactiveFlags.RAW]
      const rawTarget = toRaw(target)
      const rawKey = toRaw(key)
      // 如果key与其原始对象不一致,说明key是响应式数据
      if (key !== rawKey) {
        // 如果不是只读的话,收集key的依赖
        !isReadonly && track(rawTarget, TrackOpTypes.GET, key)
      }
      // 收集key的原始值的依赖
      !isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
      const { has } = getProto(rawTarget)
      // 确定包装函数
      // 如果是浅层响应式,包装函数返回入参:(value) => value
      // 如果是只读的响应式,包装函数就是会将value转为readonly
      // 否则使用reactive将value转为reactive
      const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
      // target的原始值自身包含key值
      if (has.call(rawTarget, key)) {
        return wrap(target.get(key)) 
      } else if (has.call(rawTarget, rawKey)) { // target的原始值自身包含key的原始值
        return wrap(target.get(rawKey))
      } else if (target !== rawTarget) { // target与原始值不同, 说明target是个响应式数据,那么继续调用target.get。例如readonly(reactive(Map))
        target.get(key)
      }
    }
    

    可以发现,如果通过get方法获取一个响应式数据对应的值时,会有两次依赖的收集,为什么这么做呢?

    其实这样做的目的是使通过响应式数据的原始值设置Map时,能够照常触发依赖。例如下面这个例子:

    const key = ref('a')
    const map = reactive(new Map())
    map.set(key, 'a')
    
    effect(() => {
      console.log(map.get(key))
    })
    map.set(key, 'b')
    map.set(toRaw(key), 'c')
    

    以上代码会依次打印a b c。无论通过key还是key的原始值进行修改Map,都能够触发依赖。

    size

    function size(target: IterableCollections, isReadonly = false) {
      // 取原始值
      target = (target as any)[ReactiveFlags.RAW]
      // 收集依赖
      !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
      return Reflect.get(target, 'size', target)
    }
    

    has

    function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
      const target = (this as any)[ReactiveFlags.RAW]
      // 取target/key的原始值
      const rawTarget = toRaw(target)
      const rawKey = toRaw(key)
      // 如果key是响应式对象,收集依赖
      if (key !== rawKey) {
        !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
      }
      // 收集依赖key的原始值对应依赖
      !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
      return key === rawKey
        ? target.has(key)
        : target.has(key) || target.has(rawKey)
    }
    

    add

    function add(this: SetTypes, value: unknown) {
      // 获取value与this的原始对象
      value = toRaw(value)
      const target = toRaw(this)
      const proto = getProto(target)
      const hadKey = proto.has.call(target, value)
      // target中不存在value时,才能触发依赖
      if (!hadKey) {
        target.add(value)
        trigger(target, TriggerOpTypes.ADD, value, value)
      }
      // 返回this,因为Set的add操纵可以链式操作
      return this
    }
    

    set

    function set(this: MapTypes, key: unknown, value: unknown) {
      value = toRaw(value)
      const target = toRaw(this)
      const { has, get } = getProto(target)
    
      // 先检查target中是否存在key,无论key值是不是响应数据
      // 如果不存在,再检查是否存在key的原始数据
      let hadKey = has.call(target, key)
      if (!hadKey) {
        key = toRaw(key)
        hadKey = has.call(target, key)
      } else if (__DEV__) {
        checkIdentityKeys(target, has, key)
      }
    
      const oldValue = get.call(target, key)
      target.set(key, value)
      // 如果不存在key 说明是新增操作,反之为修改操作
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
      // 返回this,因为Map的set操纵可以链式操作
      return this
    }
    

    为什么两次检查key的存在?

    保证使用响应式数据作为key向Map中添加数据和使用响应式数据的原始值作为key向Map中修改数据时,修改的是同一个key的数据。例如下面这个例子

    const key = reactive({})
    const map = reactive(new Map())
    map.set(toRaw(key), 'c')
    map.set(key, 'b')
    
    console.log(map.size) // 1
    

    delete

    function deleteEntry(this: CollectionTypes, key: unknown) {
      const target = toRaw(this)
      const { has, get } = getProto(target)
      let hadKey = has.call(target, key)
      // 如果target中没有key,再寻找是否有key的原始值,与set相同
      if (!hadKey) {
        key = toRaw(key)
        hadKey = has.call(target, key)
      } else if (__DEV__) {
        checkIdentityKeys(target, has, key)
      }
    
      const oldValue = get ? get.call(target, key) : undefined
      // 进行删除
      const result = target.delete(key)
      if (hadKey) {
        trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
      }
      return result
    }
    

    clear

    function clear(this: IterableCollections) {
      const target = toRaw(this)
      const hadItems = target.size !== 0
      const oldTarget = __DEV__
        ? isMap(target)
          ? new Map(target)
          : new Set(target)
        : undefined
      // forward the operation before queueing reactions
      const result = target.clear()
      // 如果集合中本就没有值,clear操作不会触发依赖
      if (hadItems) {
        trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
      }
      return result
    }
    

    forEach

    forEach函数由createForEach创建。createForEach接收两个参数:isReadonly(是否是只读响应式)、isShallow(是否是浅层响应式)。

    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)
        const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
        !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
        return target.forEach((value: unknown, key: unknown) => {
          // 包装value及key,使在forEach中访问到的key与value的响应性质与this保持一致
          return callback.call(thisArg, wrap(value), wrap(key), observed)
        })
      }
    }
    

    shallowInstrumentations

    shallowInstrumentationsshallowInstrumentations相似,只不过在生成getcreateForEach函数时,传递的参数不一样。

    const shallowInstrumentations: Record<string, Function> = {
      get(this: MapTypes, key: unknown) {
        return get(this, key, false, true)
      },
      get size() {
        return size(this as unknown as IterableCollections)
      },
      has,
      add,
      set,
      delete: deleteEntry,
      clear,
      forEach: createForEach(false, true)
    }
    

    readonlyInstrumentations

    const readonlyInstrumentations: Record<string, Function> = {
      get(this: MapTypes, key: unknown) {
        return get(this, key, true)
      },
      get size() {
        return size(this as unknown as IterableCollections, true)
      },
      has(this: MapTypes, key: unknown) {
        return has.call(this, key, true)
      },
      add: createReadonlyMethod(TriggerOpTypes.ADD),
      set: createReadonlyMethod(TriggerOpTypes.SET),
      delete: createReadonlyMethod(TriggerOpTypes.DELETE),
      clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
      forEach: createForEach(true, false)
    }
    

    readonlyInstrumentations对象是用来处理只读响应式数据的,所以所有可修改集合的操作都会通过操作失败。这些可以修改集合的操作函数都会被一个createReadonlyMethod函数生成。

    createReadonlyMethod函数接接收一个type参数,并返回一个匿名函数。

    function createReadonlyMethod(type: TriggerOpTypes): Function {
      return function (this: CollectionTypes, ...args: unknown[]) {
        if (__DEV__) {
          const key = args[0] ? `on key "${args[0]}" ` : ``
          console.warn(
            `${capitalize(type)} operation ${key}failed: target is readonly.`,
            toRaw(this)
          )
        }
        return type === TriggerOpTypes.DELETE ? false : this
      }
    }
    

    shallowReadonlyInstrumentations

    const shallowReadonlyInstrumentations: Record<string, Function> = {
      get(this: MapTypes, key: unknown) {
        return get(this, key, true, true)
      },
      get size() {
        return size(this as unknown as IterableCollections, true)
      },
      has(this: MapTypes, key: unknown) {
        return has.call(this, key, true)
      },
      add: createReadonlyMethod(TriggerOpTypes.ADD),
      set: createReadonlyMethod(TriggerOpTypes.SET),
      delete: createReadonlyMethod(TriggerOpTypes.DELETE),
      clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
      forEach: createForEach(true, true)
    }
    

    除了重写了以上几个方法外,还对keysvalues等方法也进行了重写:

    const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
    iteratorMethods.forEach(method => {
      mutableInstrumentations[method as string] = createIterableMethod(
        method,
        false,
        false
      )
      readonlyInstrumentations[method as string] = createIterableMethod(
        method,
        true,
        false
      )
      shallowInstrumentations[method as string] = createIterableMethod(
        method,
        false,
        true
      )
      shallowReadonlyInstrumentations[method as string] = createIterableMethod(
        method,
        true,
        true
      )
    })
    

    keysvaluesentriesSymbol.iterator的重写函数均通过一个createIterableMethod函数生成。

    Symbol.iterator是什么?

    集合的Symbol.iterator函数可以用来获取迭代器对象,正是因为集合实现了Symbol.iterator方法,所以可以使用for...of进行迭代。而这里需要重写Symbol.iterator方法,目的是为了实现使用for...of迭代代理对象。如下:

    const map = reactive(new Map([['a', 1], ['b', 2]]))
    
    for(const [key, value] of map) {
      console.log(key, value)
    }
    

    createIterableMethod接收三个参数:methodisReadonlyisShallow

    function createIterableMethod(
      method: string | symbol,
      isReadonly: boolean,
      isShallow: boolean
    ) {
      return function (
        this: IterableCollections,
        ...args: unknown[]
      ): Iterable & Iterator {
        const target = (this as any)[ReactiveFlags.RAW]
        const rawTarget = toRaw(target)
        const targetIsMap = isMap(rawTarget)
        // 根据isPair判断迭代时的参数
        const isPair =
          method === 'entries' || (method === Symbol.iterator && targetIsMap)
        const isKeyOnly = method === 'keys' && targetIsMap
        // 获取迭代器
        const innerIterator = target[method](...args)
        // 包装函数
        const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
        // 依赖收集
        !isReadonly &&
          track(
            rawTarget,
            TrackOpTypes.ITERATE,
            isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
          )
        // 返回一个同时满足迭代器协议和可迭代协议的对象
        return {
          // 迭代器协议
          next() {
            // 调用原始对象的迭代器的next方法获取value与done
            const { value, done } = innerIterator.next()
            return done
              ? { value, done }
              : {
                  // 包装key、value
                  value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
                  done
                }
          },
          // 实现可迭代协议,意味着可以使用for...of迭代map.keys/values/entries()
          [Symbol.iterator]() {
            return this
          }
        }
      }
    }
    

    总结

    reactive通过Proxy代理原始对象,通过拦截代理对象的操作进行依赖的收集与触发。当对代理对象进行读取操作时,进行依赖的收集;对代理对象进行修改操作则触发依赖,无论是读取操作还是修改操作,其实都是操作的原始对象,为了在执行修改操作时不污染原始对象,都会先调用toRaw获取value的原始值,然后再进行修改。

    reactive的实现是懒惰的,如果不访问代理对象的属性,那么永远不会将代理对象的属性转为代理对象。

    reactive流程:

    reactive.png

    相关文章

      网友评论

        本文标题:【vue3源码】八、reactive——Collection的响

        本文链接:https://www.haomeiwen.com/subject/jrxsgrtx.html