Vue3.0源码解析「reactive」篇 — 6.collectionHandler

556 阅读8分钟

collectionHandler

collectionHandler 定义了一系列将集合转变为某种(响应、浅响应、只读、只读浅响应)代理对象的拦截器;

CollectionType

先来看看 collection 的定义,IterableCollections 就是有遍历器接口的集合类型 Map&SetWeakCollections指的是弱引用集合 WeakMap&WeakSet

export type CollectionTypes = IterableCollections | WeakCollections

type IterableCollections = Map<any, any> | Set<any>
type WeakCollections = WeakMap<any, any> | WeakSet<any>
type MapTypes = Map<any, any> | WeakMap<any, any>
type SetTypes = Set<any> | WeakSet<any>

CollectionHandlers

mutableCollectionHandlers 为集合类型创建响应式遍历器:

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

shallowCollectionHandlers 为集合类型创建浅层响应式遍历器:

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

readonlyCollectionHandlers 为集合类型创建只读响应式遍历器:

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

shallowReadonlyCollectionHandlers 为集合类型创建浅层只读响应式遍历器:

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

这几个集合处理器都只代理了 get 操作,这是因为集合类型的响应式是通过拦截方法来实现的,使用 mp['foo'] 对属性访问或者修改是不会有响应式效果的:

let m = new Map([['foo', 'bar']]);
let mp = reactive(m);

let numpy;
watchEffect(() => { numpy = mp.get('foo'); }, { flush: 'sync' });

console.log(numpy);
mp.set('foo', 'xxx');
console.log(numpy);

这几个方法使用不同参数调用 createInstrumentationGettercreateInstrumentationGetter 根据只读和浅层选项选择 Instrumentations返回一个 get proxy

  • 这个方法里先是对几个 ReactiveFlags 标志位的拦截,同样的 RAW 在这里缓存了实体对象 target
  • 然后调用了 Reflect.get 在这里通过 hasOwn(instrumentations, key) 判断这个属性(方法)是否存在于 instrumentations 上,如果在那就用instrumentations 代替 target。这样最后被调用的类似 get/set/hasOwn.. 的方法就是我我们卸载 instrumentations 上的了。
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
      ? readonlyInstrumentations
      : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    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
    )
  }
}

interface Iterable {
  [Symbol.iterator](): Iterator
}

interface Iterator {
  next(value?: any): IterationResult
}

interface IterationResult {
  value: any
  done: boolean
}

Instrumentations

mutableInstrumentations

mutableInstrumentations是为响应式代理设计的 Instrumentation,主要重写了getsizehasaddsetdeleteclearforEach

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)
}

Record<string, Function> 这个签名还是挺有意思的:

type fO = Record<string, Function>; // { [propNames: string]: Function }
  
type Record<K extends keyof any, T> = {
	[P in K]: T;
};

shallowInstrumentations

浅响应式代理工具只是在 getforEach 函数上传入了 isShallow 标记,因为这两个函数默认对嵌套的对象进行响应式处理,而 shallow 会阻止这种行为:

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

只读响应式代理会在 get、size、has、forEach 上设置 isReadonly 标记阻止 track 记录副作用;

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)
}

而对于 add、set、delete、clear这些方法,只读式代理不允许这些更改操作,除了 delete 操作返回 this 对象之外都返回操作结果 false

function createReadonlyMethod(type: TriggerOpTypes): Function {
  return function(this: CollectionTypes, ...args: unknown[]) {
    return type === TriggerOpTypes.DELETE ? false : this
  }
}

shallowReadonlyInstrumentations

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)
}

Method Proxys

下面具体来看看上面方法的拦截都是怎么实现的:

Iterator Methods Proxy

keys、values、entries、Symbol.iterator 这几个iteratorMethods的拦截是通过一个 foreach 在初始化的时候加上去的由 createIterableMethod 这个方法创造:

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
  )
})

原生实现方案

先来看看原生的 Iterator 的实现方案,我们在 map、set、array 的签名里都能看到这几个方法:

interface Set<T> {
    [Symbol.iterator](): IterableIterator<T>;
    entries(): IterableIterator<[T, T]>;
    keys(): IterableIterator<T>;
    values(): IterableIterator<T>;
}

具体介绍一下,有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6的数组、SetMap都部署了以下三个方法,调用后都返回遍历器对象。

  • entries() 返回一个IterableIterator遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 iterator 接口,默认就是调用 entries 方法。
  • keys() 返回一个IterableIterator遍历器对象,用来遍历所有的键名。
  • values() 返回一个IterableIterator遍历器对象,用来遍历所有的键值。

这个方法的内部实现大概是这个样子:

class KeyAbleArray<T> extends Array<T> {
    keys(): IterableIterator<number> {
        let idx = 0,
            len = this.length;
        return {
            next(): IteratorResult<number> {
                return idx < len
                    ? { value: idx++, done: false }
                    : { value: undefined, done: true };
            },
            [Symbol.iterator]() {
                return this;
            },
        };
    }
}

调用这个 keys 在原有数据结构基础上计算出新的遍历结构,注意 [Symbol.iterator] 中的 this 指向的是 ak.keys() 返回的 ak

let arr = new KeyAbleArray(1, 2);
let ak = ak.keys();

console.log(ak.next()); // { value: 0, done: false }
console.log(ak.next()); // { value: 1, done: false }
console.log(ak.next()); // { value: undefined, done: true }

指的注意的是 IterableIterator,他是一个有 [Symbol.iterator] 签名的 Iterator 子类型,既要有 [Symbol.iterator] 属性也要有 Iterator 约束的方法,下面是 lib.d.ts 里的定义:

interface IterableIterator<T> extends Iterator<T> {
    [Symbol.iterator](): IterableIterator<T>;
}

interface Iterator {
  next(value?: any): IterationResult
  //...
}

下面是 vue 里重写的 Iterable ,和原生的基本一致:

interface Iterable {
  [Symbol.iterator](): Iterator
}

interface Iterator {
  next(value?: any): IterationResult
}
 
type IterableIterator = Iterable & Iterator

vue拦截方案

回到 createIterableMethod,理解了上面的原理其实这个方法很简单,调用 target[method](...args) 拿到 innerIterator,然后根据 isReadonly 判定是否执行 ITERATE track

最后返回重新包装的 Iterable & Iterator(过程看注释)。

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)
    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 a wrapped iterator which returns observed versions of the
    // values emitted from the real iterator
    return {
      // iterator protocol
      next() {
        const { value, done } = innerIterator.next()
        // 调用原生 key|entries|values 演算出结构的 next();
        return done
          ? { value, done }
          : {
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
          		// 自动嵌套响应式
              done
            }
      },
      // iterable protocol
      // 这个函数只是为了符合 iterable 部分的函数签名;
      [Symbol.iterator]() {
        return this
      }
    }
  }
}

Get proxy

get 操作首先 ReactiveFlags.RAW 上拿到实际内容缓存,调用 toRaw 防止多层代理。由于访问的键可能是响应式对象,所以也要调用 toRaw。非 readonly 状态则调用 track跟踪 effect

接下来先是通过 isShallowisReadonly 选择对嵌套对象的 nested 操作,toReactivetoReadonly 无非就是给嵌套的对象加一层响应式代理,toShallow 则无任何操作。

  • 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
    

最后先提取源对象原型链上的 has(这里之所以要在原型链上拿 has 是为了防止嵌套的响应式拦截),来进行判断:

  • has.call(rawTarget, key)wrap(target.get(key))通过 key拿到键值并用 wrap 处理;
  • has.call(rawTarget, rawKey)wrap(target.get(rawKey))通过 rawKey拿到键值并用 wrap 处理;
  • target !== rawTarget:嵌套的响应式对象,即 target 已经是响应式对象,直接调用 target.get
function get(
  target: MapTypes,
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  // #1772: readonly(reactive(Map)) should return readonly + reactive version of the value
  target = (target as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  if (key !== rawKey) {
    !isReadonly && track(rawTarget, TrackOpTypes.GET, key)
  }
  !isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  const { has } = getProto(rawTarget)
  if (has.call(rawTarget, key)) {
    return wrap(target.get(key))
  } else if (has.call(rawTarget, 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
    target.get(key)
  }
}

const getProto = <T extends CollectionTypes>(v: T): any =>
  Reflect.getPrototypeOf(v)

Has proxy

hasget的逻辑基本相同,只是 has 触发的 trackTrackOpTypesHAS

function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
	const target = (this as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  if (key !== rawKey) {
    !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
  }
  !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
  return key === rawKey
    ? target.has(key)
    : target.has(key) || target.has(rawKey)
}

Size proxy

sizehas 也差不多,只是 sizecollection 上是以 get size的形式存在所以要通过 Reflect.get(target, 'size', target) 访问:

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)
}

Add proxy

add 调用 target.add(value) 然后进行 trigger ADD:

function add(this: SetTypes, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  if (!hadKey) {
    target.add(value)
    trigger(target, TriggerOpTypes.ADD, value, value)
  }
  return this
}
Set Proxy

Settarget 添加属性,根据属性为新属性 (hadKey) 以及属性是否为旧属性值变化来调用 SET trigger 还是 ADD trigger

function set(this: MapTypes, key: unknown, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  const { has, get } = getProto(target)

  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, 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
}

Delete Proxy

deleteEntry 为属性删除代理,target.delete(key) 删除属性,并且根据 target.has(key) 来决定是否触发类型为 DELETEtrigger

function deleteEntry(this: CollectionTypes, key: unknown) {
  const target = toRaw(this)
  const { has, get } = getProto(target)
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  } 
  
  const oldValue = get ? get.call(target, key) : undefined
  // forward the operation before queueing reactions
  const result = target.delete(key)
  if (hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}
Clear Proxy

clear 代理先调用 target.clear() 清空集合结构,然后根据 hadItems 决定是否执行 CLEAR trigger

function clear(this: IterableCollections) {
  const target = toRaw(this)
  const hadItems = target.size !== 0
  const oldTarget = undefined
  // forward the operation before queueing reactions
  const result = target.clear()
  if (hadItems) {
    trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
  }
  return result
}

ForEach Proxy

createForEach 返回一个代理后的 forEach 方法,首先执行 ITERATE trigger(非 readonly 状态下),然后调用 target.forEach 内部执行 callback,因为 forEach 也是一种访问操作,所以要嵌套响应式属性,即wrap(value),不过这里还 wrap(key) 了,不知为何。

要注意的就是 forEach 的第三个参数为 callbackthis,所以我们在调用 callback 的时候要强绑定这个 this

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) => {
      // 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)
    })
  }
}