- A+
源码位置:https://github.com/vuejs/core/blob/main/packages/reactivity/src/collectionHandlers.ts
这个文件主要用于处理Set
、Map
、WeakSet
、WeakMap
类型的拦截。
拦截是为了什么?为什么要处理这些方法?
Vue3
实现响应式的思路是使用Proxy
API在getter
中收集依赖,在setter
触发更新。
而Set
、Map
等这些内置集合类型比较特殊,举个例子,我们在使用Map
的实例对象的时候,我们一般不会在实例对象上面去添加属性或者修改自定义属性的值,而是通过其原型上的get/set
方法来操作键值对。
值得注意的是,我们仅通过调用原型上的方法来操作键值对,而不会去修改实例对象上的属性。因此,我们仅需要给Proxy
配置getter
,不需要配置setter
。
const map = new Map<any, any>(); // √ map.set('k1', 'v1'); map.get('k1'); // × map.k1 = 'v1'; map.k1;
而Vue3实现响应式的需求是希望调用get/set
方法也能正确地收集依赖、触发更新。因此,需要对这些方法进行改造。
从响应式原理的角度出发,我们需要思考对集合的读和写操作:
- 在读的时候收集依赖:与读操作相关的方法,内部要执行
track
收集依赖;- 与读操作相关的方法:
get
、has
、size
(这个是属性,也要处理)、forEach
,以及返回迭代器对象的其它方法;
- 与读操作相关的方法:
- 在写的时候触发更新:与写操作相关的方法,内部要执行
trigger
函数触发更新;- 与写操作相关的方法:
add
、set
、delete
、clear
。
- 与写操作相关的方法:
返回迭代器对象的方法有:
const iteratorMethods = [ 'keys', 'values', 'entries', Symbol.iterator, ] as const
其中
Symbol.iterator
是为了实现for of
遍历必须实现的接口,在JavaScript
中的所有可迭代对象都要实现这个接口。
正式开始阅读代码
这个文件的代码结构和baseHandlers
不太一样,这个文件是先分别实现对get
、set
、has
、size
等操作的拦截,然后再整合成一个getter
返回。
根据是否是shallow
和readonly
分别导出了四种handler
:
mutableCollectionHandlers
shallowCollectionHandlers
readonlyCollectionHandlers
shallowReadonlyCollectionHandlers
export
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = { get: /*#__PURE__*/ createInstrumentationGetter(false, false), } export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = { get: /*#__PURE__*/ createInstrumentationGetter(false, true), } export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = { get: /*#__PURE__*/ createInstrumentationGetter(true, false), } export const shallowReadonlyCollectionHandlers: ProxyHandler<CollectionTypes> = { get: /*#__PURE__*/ createInstrumentationGetter(true, true), }
可以看到这些Handlers
都是通过createInstrumentationGetter
来返回getter
,接下来看看createInstrumentationGetter
内部是如何实现的。
createInstrumentationGetter
源码:(分段解析在下面)
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) { // 根据是否是只读和是否是浅层响应式来选择不同的处理函数集 const instrumentations = shallow ? isReadonly ? shallowReadonlyInstrumentations // 浅层只读的处理函数集 : shallowInstrumentations // 浅层的处理函数集 : isReadonly ? readonlyInstrumentations // 只读的处理函数集 : mutableInstrumentations // 可变(非只读)的处理函数集 // 返回一个自定义的getter函数,用于处理特定的键 return ( target: CollectionTypes, // 目标集合类型 key: string | symbol, // 被访问的键 receiver: CollectionTypes, // 代理或包装过的集合 ) => { // 检查特殊标志键 if (key === ReactiveFlags.IS_REACTIVE) { // 如果不是只读的,返回true表示是响应式的 return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { // 如果是只读的,返回true表示是只读的 return isReadonly } else if (key === ReactiveFlags.RAW) { // 返回原始的目标集合 return target } // 使用Reflect.get来获取值 // 如果instrumentations有这个键,并且这个键在目标集合中,则从instrumentations获取 // 否则直接从目标集合获取 return Reflect.get( hasOwn(instrumentations, key) && key in target ? instrumentations : target, key, receiver, ) } }
分段解读代码:
// 根据是否是只读和是否是浅层响应式来选择不同的处理函数集 const instrumentations = shallow ? isReadonly ? shallowReadonlyInstrumentations // 浅层只读的处理函数集 : shallowInstrumentations // 浅层的处理函数集 : isReadonly ? readonlyInstrumentations // 只读的处理函数集 : mutableInstrumentations // 可变(非只读)的处理函数集
这个函数根据isReadonly
和isShallow
选择了不同的函数集,函数集里的函数是特殊处理过的,目的是为了使这些实例方法可以适应Vue
的响应式系统。
// 检查特殊标志键 if (key === ReactiveFlags.IS_REACTIVE) { // 如果不是只读的,返回true表示是响应式的 return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { // 如果是只读的,返回true表示是只读的 return isReadonly } else if (key === ReactiveFlags.RAW) { // 返回原始的目标集合 return target }
对于Vue
内部特有的key
,比如ReactiveFlags
,返回特定的内容。这些ReactiveFlags
并不存在于对象上,只是在getter
做拦截并返回。
// 使用Reflect.get来获取值 // 如果instrumentations有这个键,并且这个键在目标集合中,则从instrumentations获取 // 否则直接从目标集合获取 return Reflect.get( hasOwn(instrumentations, key) && key in target ? instrumentations : target, key, receiver, )
最后,使用Reflect.get
方法执行对key
的访问并返回。这个时候会通过hasOwn(instrumentations, key)
检查访问key
是否在生成的函数集里:
- 如果存在,那么应该应用特殊处理过的函数集里的函数;
- 如果不存在,那么就用
target
身上原始的方法。
createInstrumentations
四种不同的函数集由createInstrumentations
函数创建并返回。
const [ mutableInstrumentations, readonlyInstrumentations, shallowInstrumentations, shallowReadonlyInstrumentations, ] = /* #__PURE__*/ createInstrumentations()
接下来是craeteInstrumentations
的实现,是一段很长的代码:
这里只展示了mutableInstrucmentations
的函数集,其它三个大同小异,其中的各种零碎的get
、size
、has
...方法的处理在后文介绍。
function createInstrumentations() { // 定义可变(非只读)的响应式处理函数集 const mutableInstrumentations: Instrumentations = { get(this: MapTypes, key: unknown) { // 获取Map中的值,默认不是只读且不是浅层 return get(this, key) }, get size() { // 获取集合的大小 return size(this as unknown as IterableCollections) }, has, // 检查集合中是否存在特定的值 add, // 向集合中添加元素 set, // 设置Map中的键值对 delete: deleteEntry, // 从集合中删除元素 clear, // 清空集合 forEach: createForEach(false, false), // 遍历集合的元素 } // 定义浅层的响应式处理函数集 const shallowInstrumentations: Instrumentations = { ... } // 定义只读的响应式处理函数集 const readonlyInstrumentations: Instrumentations = { ... } // 定义浅层只读的响应式处理函数集 const shallowReadonlyInstrumentations: Instrumentations = { ... } // 定义迭代器方法列表 const iteratorMethods = [ 'keys', 'values', 'entries', Symbol.iterator, ] as const // 为每个迭代器方法添加对应的响应式处理函数 iteratorMethods.forEach(method => { mutableInstrumentations[method] = createIterableMethod(method, false, false) readonlyInstrumentations[method] = createIterableMethod(method, true, false) shallowInstrumentations[method] = createIterableMethod(method, false, true) shallowReadonlyInstrumentations[method] = createIterableMethod( method, true, true, ) }) // 返回包含所有处理函数集的数组 return [ mutableInstrumentations, // 可变(非只读)的处理函数集 readonlyInstrumentations, // 只读的处理函数集 shallowInstrumentations, // 浅层的处理函数集 shallowReadonlyInstrumentations, // 浅层只读的处理函数集 ] }
注意到迭代器方法也都做了特殊处理,这是因为迭代器方法返回迭代器对象,而不是操作对象本身,无法被Proxy
拦截,故无法追踪依赖。
这里使用了createIterableMethod
创建能够适配响应式的版本。
createIterableMethod
返回迭代器对象的几个需要处理的方法分别是:
keys
values
entries
Symbol.iterator
前三个是string
类型传入,最后一个是symbol
类型传入。
源码:
function createIterableMethod( method: string | symbol, // 迭代器方法名,可以是字符串或符号 isReadonly: boolean, // 是否为只读的迭代器 isShallow: boolean, // 是否为浅层迭代器 ) { // 返回一个自定义的迭代器方法 return function ( this: IterableCollections, // 当前的集合对象 ...args: unknown[] // 方法调用的参数 ): Iterable<unknown> & Iterator<unknown> { // 获取原始的集合对象 const target = (this as any)[ReactiveFlags.RAW] const rawTarget = toRaw(target) const targetIsMap = isMap(rawTarget) // 判断目标是否为Map类型 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 { // 实现迭代器 next() { const { value, done } = innerIterator.next() // 调用内部迭代器的next方法 return done ? { value, done } // 如果迭代完成,返回当前值和完成标志 : { value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), // 如果迭代未完成,返回包装过的值 done, // 完成标志保持不变 } }, // 实现可迭代协议 [Symbol.iterator]() { return this }, } } }
Vue3
在处理Map
和Set
的时候并没有分开处理,而是一起处理了,因为它们有许多名字相同的方法,分开处理可能会导致代码更乱。
对于entries
的输出,也就是[key, value]
格式的遍历,通过简单的判断处理了:
const isPair = method === 'entries' || (method === Symbol.iterator && targetIsMap)
方法处理
这里仅记录get
和set
方法。
get
get
方法是Map
和WeakMap
独有的,所以target
类型是MapTypes
。
查询的target
和key
都可能是响应式对象,都需要做toRaw
获取原始值。如果直接在响应式对象上做操作,则可能被Proxy
捕获到,从而记录了不必要的依赖。
返回值的时候需要根据target
的类型进行对应的包装,即toReactive
、toShallow
或toReadonly
。
这是因为使用set
的时候存的是rawValue
,而返回的时候需要配合target
的类型。
源码:
function get( target: MapTypes, // 目标对象,类型是 MapTypes key: unknown, // 要获取值的键,类型是 unknown isReadonly = false, // 是否只读,默认值为 false isShallow = false // 是否浅层响应,默认值为 false ) { // 确保如果 target 是响应式对象,操作的是它的原始对象 target = (target as any)[ReactiveFlags.RAW] // 获取 target 的原始对象 const rawTarget = toRaw(target) // 获取 key 的原始值 const rawKey = toRaw(key) if (!isReadonly) { // 如果 key 与 rawKey 不同(即 key 是响应式对象),跟踪对 key 的访问 if (hasChanged(key, rawKey)) { track(rawTarget, TrackOpTypes.GET, key) } // 跟踪对 rawKey 的访问 track(rawTarget, TrackOpTypes.GET, rawKey) } // 获取 target 原型上的 has 方法 const { has } = getProto(rawTarget) // 根据 isShallow 和 isReadonly 选择对应的包装函数 const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive // 如果原始对象上存在 key,则返回包装后的值 if (has.call(rawTarget, key)) { return wrap(target.get(key)) // 如果原始对象上存在 rawKey,则返回包装后的值 } else if (has.call(rawTarget, rawKey)) { return wrap(target.get(rawKey)) // 如果 target 不是原始对象,则调用 target.get(key) 进行跟踪 } else if (target !== rawTarget) { // 确保在只读的响应式 Map 中,嵌套的响应式 Map 也能进行依赖跟踪 target.get(key) } }
最后为什么还要加一个判断target!==rawTarget
?
这个判断和一个bug
有关:readonly() breaks reactivity of Map · Issue #3602 · vuejs/core (github.com)
背景:
在 Vue3 的响应式系统中,
readonly
和reactive
组合使用时可能会出现一些问题,特别是在处理嵌套结构时。例如,当你有一个readonly
包装的reactive Map
,并试图在这个Map
中获取一个值,如果不进行额外处理,可能会导致嵌套的响应式Map
无法正确进行依赖跟踪。示例代码:
const reactiveMap = reactive(new Map([['key', new Map([['nestedKey', 'value']])]])); const readonlyMap = readonly(reactiveMap); // 获取嵌套的 Map const nestedMap = readonlyMap.get('key'); // 尝试获取嵌套 Map 的值 const value = nestedMap.get('nestedKey');
在这种情况下,如果不进行额外处理,
nestedMap
可能无法正确进行依赖跟踪。因为直接操作readonly
包装的对象不会触发响应式系统的依赖跟踪。这意味着当nestedKey
的值发生变化时,可能不会触发相关的响应式更新。解决方法:
判断
target
是否是响应式对象,如果是的话,手动调用get
触发对依赖的收集。注意到
rawTarget
是由toRaw(target)
得到的,接下来看一下toRaw
函数的实现:
toRaw
的源码位置:core/packages/reactivity/src/reactive.ts at main · vuejs/core (github.com)export function toRaw<T>(observed: T): T { // 尝试获取raw对象 const raw = observed && (observed as Target)[ReactiveFlags.RAW] // 如果存在raw对象,则递归调用;如果不存在,则表示当前的observed已经是原始对象 return raw ? toRaw(raw) : observed }
可以看到如果传入的对象如果有
ReactiveFlags.RAW
这个key
,就认为它是被Vue
包装过的对象,因为只有被reactive
、readonly
等API包装过的对象会被Vue
添加上ReactiveFlags.RAW
属性,记录着原始对象的引用。这里需要递归调用是因为对象可能被多层包装,比如
readonly(reactive({}))
。回到Map的get方法的最后处理:
if (target !== rawTarget) { // 确保在只读的响应式 Map 中,嵌套的响应式 Map 也能进行依赖跟踪 target.get(key) }
如果
target === rawTarget
,则target
是原始对象;如果
target!==rawTarget
,则target
是包装过的对象,可能是reactive
包装过的响应式对象,也可能是readonly
包装过的只读对象;这里或许可以再优化?如果是只读对象,就不追踪依赖了。
set
Map
的key
可能是原始值也可能是响应式对象,这里需要做类型判断,并且对原始key
和响应式key
都做判断。
在开发环境下如果存在同一个原始对象的两种类型的key,会输出警告。
因为这种不规范的写法会保存两份键值对,内容可能不一致。
源码:
function set(this: MapTypes, key: unknown, value: unknown, _isShallow = false) { // 如果值不是浅层的且不是只读的,则获取其原始值 if (!_isShallow && !isShallow(value) && !isReadonly(value)) { 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) } else if (__DEV__) { // 在开发环境中,检查键的类型是否一致 checkIdentityKeys(target, has, 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) } // 返回 this 以支持链式调用 return this }