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 few
ReactiveFlags
Flag bit interception, same thingRAW
This is where the entity object is cachedtarget
: - And then called
Reflect.get
It goes through herehasOwn(instrumentations, key)
Determines whether the property (method) exists ininstrumentations
Go. Use it if it’s thereinstrumentations
Instead oftarget
. That’s how it ends up being calledget/set/hasOwn..
The way is we unloadinstrumentations
On 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 aIterableIterator
Traverser 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.Map
The structure of theiterator
Interface, which is called by defaultentries
Methods.keys()
Returns aIterableIterator
Iterator object that iterates over all key names.values()
Returns aIterableIterator
Iterator 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
vue
Interception 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))
throughkey
Get the key and use itwrap
Processing;has.call(rawTarget, rawKey)
:wrap(target.get(rawKey))
throughrawKey
Get the key and use itwrap
Processing;target ! == rawTarget
: nested responsive objects, that istarget
It’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