Vue3 源码解析 06下篇--响应式 collectionHandler
前言
这里是handler的下篇 上篇看这里:Vue3 源码解析 06上篇--响应式 baseHandler
collectionHandlers
在看源码之前我们顺便提一下 Proxy 的局限性
Proxy 的局限性
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1)
这里会报错:Uncaught TypeError: Method Map.prototype.set called on incompatible receiver [object Object]
具体原因我们可以参考一下Proxy 的局限性,这跟内置对象(例如 Map、Set、Date、Promise)的内部机制有关,他们的内部所有的数据存储在一个“internal slots”中。当我们访问 Set.prototype.add 其实就是通过内部的 this 来访问该方法的,但是数据代理的时候this=proxy,但是 proxy 并没有相应的“internal slots”这个东西,所以会报错。 所以,我们可以用另一种方式来实现代理:
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
proxy.set('test', 1);
alert(proxy.get('test'));//1
我们通过这种方式,改变了 this 的指向,这时候的 this 绑定为了原始对象(即 Map),而不是之前说的 proxy。所以就避开了上面的坑。
mutableCollectionHandlers
collectionHandlers 针对的是集合数据类型(即 set、map、weakSet、weakMap)。其主要包含三种:mutableCollectionHandlers(普通响应式数据)shallowCollectionHandlers(浅层响应式数据)、readonlyCollectionHandlers(只读响应式数据)
因为这三者的实现都是大同小异的。所以我们这里先看一下 mutableCollectionHandlers 的源码:
//packages/reactivity/src/collectionshandlers.ts
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
/**
* 这里我们就可以理解,为什么集合类型的代理只有一个get了
*/
get: createInstrumentationGetter(false, false)
}
上面的源码很简单,主要就是一个 createInstrumentationGetter,所以下面我们看一下:
//该方法的主要作用就是通过instrumentations实现数据劫持
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
//根据需求,instrumentations取不同的值
const instrumentations = shallow
? shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
//返回一个处理后的get方法
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
//根据key的类型返回不同的数据
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.RAW) {
return target
}
//返回Reflect.get()处理后的方法
//如果是 get has add set delete clear forEach 的方法调用,或者是获取size,那么改为调用mutabelInstrumentations里的相关方法
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}
上面这段代码的核心就是我们之前提到修改 Proxy 缺陷的部分,通过修改 this 的指向来完成代理
下面我们来看一下 mutableInstrumentations 是如何实现数据劫持的:
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)
}
这段代码的意义还是我们之前说的那样,创建一个新的对象,这个对象有和 set、map 相同的方法名。这些方法对应的就是我们代理后,注入了依赖收集跟响应触发的方法。然后通过 Reflect.get 反射到我们的原始对象上,这样我们就可以通过新创建的对象来操作原始对象了,同时达到了数据劫持的目的。
下面我们简单看一下几个主要的方法:
get 方法
function get(
target: MapTypes,
key: unknown,
isReadonly = false,
isShallow = false
) {
//获取原始数据
target = (target as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
//获取原始的key,这里是因为Map中也可以使用对象作为key,所以对象也可能是响应式的
const rawKey = toRaw(key)
//如果key和rawKey不同,说明key也是响应式的,需要对key进行依赖收集
if (key !== rawKey) {
!isReadonly && track(rawTarget, TrackOpTypes.GET, key)
}
//对rawKey进行依赖收集
!isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
const { has } = getProto(rawTarget)
const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
//判断是否存在key和rawKey,如果存在则进行wrap处理
//这里的warp其实就是toReactive方法,将获取的value值转换为响应式的数据
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
}
}
简单梳理一下 get 方法:
- 首先获取原始的 target 数据 value 和 key
- 如果 key 也是响应式数据的话,对 key 进行依赖收集
- 对数据进行响应是处理并返回处理后的数据(即 Reactive 对象)
其实简单来说就是我们调用 get 方法的时候获取到的就已经是响应式的数据了。
size
size 方法比较简单,毕竟 size 只是一个属性:
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)
}
- 获取原始数据
- 触发 iterate 类型的依赖收集
- 返回原型上面的 size
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)
}
从代码结构就可以看出,has 和 get 是差不多的。对 key 和 rawKey 进行 track(依赖收集)
add
add 方法主要针对的是 Set 类型的数据,它 的代码跟 baseHandlers 的逻辑是差不多的,
function add(this: SetTypes, value: unknown) {
//获取原始数据
value = toRaw(value)
const target = toRaw(this)
//获取原型
const proto = getProto(target)
//通过原型上的方法判断是否已经存在当前数据
const hadKey = proto.has.call(target, value)
//通过原型上的方法添加数据
const result = target.add(value)
//如果不存在当前数据,说明是新增的,需要触发响应式
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, value, value)
}
return result
}
我们简单梳理一下 add 方法:
- 获取原始数据和原型
- 通过原型方法添加数据
- 判断是否已经存在当前 key,如果不存在则需要额外触发响应式逻辑
set
set 方法主要针对的是 Map 类型的数据,
function set(this: MapTypes, key: unknown, value: unknown) {
//获取原始数据和原型
value = toRaw(value)
const target = toRaw(this)
//获取原型上面的方法
const { has, get } = getProto(target)
//原型方法判断是否有存在当前key
let hadKey = has.call(target, key)
//进一步获取原始key进行校验
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
//开发环境检查目标数据上面是否同时存在rawKey和key,这样可能会数据不一致
checkIdentityKeys(target, has, key)
}
//获取原始数据,添加新数据
const oldValue = get.call(target, key)
const result = target.set(key, value)
//根据是否存在原始的key,来判断调用什么类型的trigger
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return result
}
set 的逻辑和 add 是大同小异的,不同的是,在 set 里面需要根据 key 和 rawKey 的存在情况进行分开处理。
delete&clear
delete 的核心逻辑就是触发 delete 的 trigger,代码和 set 都是大同小异的,这里就不过多介绍了。clear 的核心就是触发 clear 的 trigger。同样是调用原型的 clear 方法进行处理。但是中途添加了target.size的判断,当 size 为 0 的时候是不会触发 trigger 的。
function clear(this: IterableCollections) {
//获取原型
const target = toRaw(this)
//size是否为0
const hadItems = target.size !== 0
//开发环境下获取数据
const oldTarget = __DEV__
? isMap(target)
? new Map(target)
: new Set(target)
: undefined
//调用原型的clear方法
const result = target.clear()
//size不为0的情况下触发trigger
if (hadItems) {
trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
}
return result
}
这里简单的梳理一下 clear 的逻辑:
- 获取原型
- 开发环境下重新获取封装后的数据
- 调用原型的 clear 方法处理数据
- 如果 size 不为 0 的时候触发 clear 类型的 trigger
forEach
forEach 方法会触发 iterate 类型的 track 方法。
//迭代器方法
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)
//根据不同的类型赋值wrap,当前就是toReactive
const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
//进行依赖收集,因为forEach会导致集合整体的变化
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
//劫持传递进来的callback方法,让传入的callback数据转换成响应式的数据
return target.forEach((value: unknown, key: unknown) => {
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
}
forEach 的核心逻辑就是劫持传入的 callback 方法,将原本传入的数据转换为响应式数据后返回
迭代器:createIterableMethod
迭代器主要是对集合中的迭代进行处理即:['keys', 'values', 'entries', Symbol.iterator]
//迭代器相关方法
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
)
readonlyInstrumentations[method as string] = createIterableMethod(...)
shallowInstrumentations[method as string] = createIterableMethod(...)
})
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)
//当前数据是否为map类型
const targetIsMap = isMap(rawTarget)
//如果是entries方法,或者是map的迭代方法,isPair为true
const isPair =
method === 'entries' || (method === Symbol.iterator && targetIsMap)
//如果是keys方法当前数据为map类型
const isKeyOnly = method === 'keys' && targetIsMap
//调用原型上的响应的迭代方法
const innerIterator = target[method](...args)
//根据描述获取响应的转换响应式数据的方法
const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
//依赖收集
!isReadonly &&
track(
rawTarget,
TrackOpTypes.ITERATE,
isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
)
return {
// iterator protocol
next() {
const { value, done } = innerIterator.next()
//如果是迭代器中的最后一个值, 不做响应式转换,反之进行相应的响应式转化
return done
? { value, done }
: {
value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
done
}
},
// iterable protocol
[Symbol.iterator]() {
return this
}
}
}
}
梳理一下迭代器的简单逻辑:
- 获取原始数据和数据类型
- 获取原型的迭代方法
- 根据我们的描述获取响应式数据方法
- 进行依赖收集
- 调用原型上的迭代方法对数据进行处理,同时添加响应式
简单来说就是在迭代的过程中添加数据响应式(最简单的理解 🤔)
小结
这里只看了一下 mutableCollectionHandlers 相关的源码,其他类型的数据其实是大同小异的,这里就不过多介绍了。 简单的总结一下 mutableCollectionHandlers 的实现:
- 集合类型的数据通过劫持原始数据的 get 行为绕过了 Proxy 的缺陷问题
- 劫持操作,一般都是获取原始数据然后获取原型方法,然后将 this 指向原始数据,再调用相关方法
- 对于查询操作,插入收集依赖的逻辑,然后返回响应式数据
- 对于修改操作,插入监听逻辑
- 对于迭代操作,插入收集依赖的逻辑,迭代过程中将数据转换成响应式的数据
简单来说,对集合的代理,就是对集合方法的代理,在集合方法执行的是后,进行不同类型的 track 或者 trigger
总结
总算是整理玩了 reactive 的逻辑,虽然过程磕磕绊绊的,但是收获还是挺大的。大神的代码非常值得拜读。 后面如果有时间的话会根据这些天看源码的经验实现一个简单的响应式。希望尽快完成