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