collectionHandler
collectionHandler 定义了一系列将集合转变为某种(响应、浅响应、只读、只读浅响应)代理对象的拦截器;
CollectionType
先来看看 collection 的定义,IterableCollections 就是有遍历器接口的集合类型 Map&Set,WeakCollections指的是弱引用集合 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);
这几个方法使用不同参数调用 createInstrumentationGetter,createInstrumentationGetter 根据只读和浅层选项选择 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,主要重写了get、size、has、add、set、delete、clear和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)
}
Record<string, Function> 这个签名还是挺有意思的:
type fO = Record<string, Function>; // { [propNames: string]: Function }
type Record<K extends keyof any, T> = {
[P in K]: T;
};
shallowInstrumentations
浅响应式代理工具只是在 get 和 forEach 函数上传入了 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的数组、Set、Map都部署了以下三个方法,调用后都返回遍历器对象。
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。
接下来先是通过 isShallow 和 isReadonly 选择对嵌套对象的 nested 操作,toReactive 和 toReadonly 无非就是给嵌套的对象加一层响应式代理,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
has 和 get的逻辑基本相同,只是 has 触发的 track 的 TrackOpTypes 为 HAS:
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 和 has 也差不多,只是 size 在 collection 上是以 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 调用 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
Set 给 target 添加属性,根据属性为新属性 (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) 来决定是否触发类型为 DELETE 的 trigger:
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 的第三个参数为 callback 的 this,所以我们在调用 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)
})
}
}