collectionHandler

The collectionHandler defines a series of interceptors that turn collections into some kind of proxy object (response, shallow response, read-only, read-only shallow response);

CollectionType

First let’s look at the definition of collection, IterableCollections are collection types of traversal interface Map&Set, WeakCollections refers to weak reference set 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>
Copy the code

CollectionHandlers

MutableCollectionHandlers create responsive traversal for collection types:

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(false.false)}Copy the code

ShallowCollectionHandlers create shallow responsive traversal for collection types:

export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(false.true)}Copy the code

ReadonlyCollectionHandlers create read-only reactive traversal for collection types:

export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(true.false)}Copy the code

ShallowReadonlyCollectionHandlers create shallow read-only reactive traversal for collection types:

export const shallowReadonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(true.true)}Copy the code

All of these collection handlers only delegate get operations, because collection types are reactive through intercepting methods, and accessing or modifying properties with mp[‘foo’] has no reactive effect:

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);
Copy the code

That a few methods use different parameter called createInstrumentationGetter, createInstrumentationGetter according to select Instrumentations returns a read-only and shallow get proxy:

  • In this method, we start with a fewReactiveFlagsFlag bit interception, same thingRAWThis is where the entity object is cachedtarget:
  • And then calledReflect.getIt goes through herehasOwn(instrumentations, key)Determines whether the property (method) exists ininstrumentationsGo. Use it if it’s thereinstrumentationsInstead oftarget. That’s how it ends up being calledget/set/hasOwn..The way is we unloadinstrumentationsOn the.
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
}

interfaceIterator { next(value? :any): IterationResult
}

interface IterationResult {
  value: any
  done: boolean
}
Copy the code

Instrumentations

mutableInstrumentations

Instrumentations are Instrumentation for get, size, has, add, set, delete, clear, and forEach:

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)}Copy the code

Record
,>

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

shallowInstrumentations

Shallow responsive proxies simply pass the isShallow flag on the GET and forEach functions because they default to reactive processing of nested objects, and shallow prevents this behavior:

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)}Copy the code

readonlyInstrumentations

Read-only responsive agents set isReadonly flags on get, size, HAS, and forEach to prevent track from recording side effects.

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)}Copy the code

For add, set, delete, and clear methods, read-only agents do not allow these changes and return the result except for delete, which returns this. False:

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

shallowReadonlyInstrumentations

ShallowReadonlyInstrumentations is the combination of above two:

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)}Copy the code

Method Proxys

Let’s see how the above methods are implemented:

Iterator Methods Proxy

IteratorMethods: Keys, values, entries, Symbol. Iterator intercepts are created by the createIterableMethod method with a foreach added at initialization:

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)})Copy the code

Native implementation

Let’s look at the native implementation of Iterator. We can see these methods in map, set, and array signatures:

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

To be specific, some data structures are computationally generated on the basis of existing data structures. For example, ES6 arrays, sets, and maps all deploy the following three methods, which return a traverser object when called.

  • entries()Returns aIterableIteratorTraverser object for traversal[Key name, key value]Is an array. For arrays, the key is the index value; forSet, the key name is the same as the key value.MapThe structure of theiteratorInterface, which is called by defaultentriesMethods.
  • keys()Returns aIterableIteratorIterator object that iterates over all key names.
  • values()Returns aIterableIteratorIterator object, used to iterate over all key values.

The internal implementation of this method looks something like this:

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; }}; }}Copy the code

Call this key to calculate the new traversal structure based on the existing data structure. Note that this in [symbol.iterator] refers to the ak returned by ak.keys() :

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 }
Copy the code

IterableIterator is a subtype of iterator signed by [Symbol. Iterator]. Iterator must have both properties and iterator constraints. Here is the definition in lib.d.ts:

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

interfaceIterator { next(value? :any): IterationResult
  / /...
}
Copy the code

Here is the Iterable rewritten in vue, which is basically the same as the original:

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

interfaceIterator { next(value? :any): IterationResult
}
 
type IterableIterator = Iterable & Iterator
Copy the code

vueInterception project

Call target[method](createIterableMethod) (target[method](… Args takes the innerIterator and then determines whether to execute ITERATE Track based on isReadonly.

Finally return the rewrapped Iterable & Iterator(see comments for the procedure).

function createIterableMethod(
  method: string | symbol,
  isReadonly: boolean,
  isShallow: boolean
) {
  return function(
    this: IterableCollections, ... args: unknown[]) :可迭代 & 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
    constinnerIterator = target[method](... args)constwrap = 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()
        / / call native key | entries | values calculus of the structure of the next ();
        return done
          ? { value, done }
          : {
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
          		// Automatic nesting response
              done
            }
      },
      // iterable protocol
      // This function is only intended to conform to the iterable part of the function signature;
      [Symbol.iterator]() {
        return this}}}}Copy the code

Get proxy

The get operation first retrieves the actual content cache on reactiveFlags. RAW and calls toRaw to prevent multiple layers of proxies. Since the keys accessed may be reactive objects, toRaw is also called. If the state is not readonly, track is called to trace effect.

After that, the nested object is selected by isShallow and isReadonly, while toReactive and toReadonly add a layer of reactive proxies to the nested object, toShallow does nothing.

  • 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
    Copy the code

Finally, extract the HAS on the prototype chain of the source object (the reason why we take HAS on the prototype chain is to prevent nested responsive interception) to judge:

  • has.call(rawTarget, key):wrap(target.get(key))throughkeyGet the key and use itwrapProcessing;
  • has.call(rawTarget, rawKey):wrap(target.get(rawKey))throughrawKeyGet the key and use itwrapProcessing;
  • target ! == rawTarget: nested responsive objects, that istargetIt’s already a reactive object, called directlytarget.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)
Copy the code

Has proxy

The logic of HAS and GET is basically the same, except that the TrackOpTypes of the track triggered by HAS is:

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)
}
Copy the code

Size proxy

Get (target, ‘size’, target) = 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)
}
Copy the code

Add proxy

Add calls target.add(value) and triggers 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
}
Copy the code
Set Proxy

Set trigger adds an attribute to target, calling Set trigger or ADD trigger depending on whether the attribute is a new attribute (hadKey) and whether the attribute is an old one:

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
}
Copy the code

Delete Proxy

Delete (key) deletes the property, and determines whether to trigger the delete trigger based on target.has(key) :

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
}
Copy the code
Clear Proxy

The clear agent calls target.clear() to clear the collection structure, and then decides whether to execute clear trigger based on hadItems:

function clear(this: IterableCollections) {
  const target = toRaw(this)
  consthadItems = 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
}
Copy the code

ForEach Proxy

CreateForEach returns a proxy forEach method, first executing ITERATE trigger(in non-readonly state), then calling target. ForEach internally executing callback, Because forEach is also an access operation, we need to nest a responsive property, wrap(value), but wrap(key), for some reason.

Note that the third parameter of forEach is this for callback, so we should bind this to callback by force.

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)
    constwrap = 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)
    })
  }
}
Copy the code