找回密码
 立即注册
首页 业界区 业界 [vue3] Vue3源码阅读笔记 reactivity - collectionHandl ...

[vue3] Vue3源码阅读笔记 reactivity - collectionHandlers

慷规扣 2025-6-6 15:54:34
源码位置: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。
  1. const map = new Map();
  2. // √
  3. map.set('k1', 'v1');
  4. map.get('k1');
  5. // ×
  6. map.k1 = 'v1';
  7. map.k1;
复制代码
而Vue3实现响应式的需求是希望调用get/set方法也能正确地收集依赖、触发更新。因此,需要对这些方法进行改造。
从响应式原理的角度出发,我们需要思考对集合的读和写操作:

  • 在读的时候收集依赖:与读操作相关的方法,内部要执行track收集依赖;

    • 与读操作相关的方法:get、has、size(这个是属性,也要处理)、forEach,以及返回迭代器对象的其它方法;

  • 在写的时候触发更新:与写操作相关的方法,内部要执行trigger函数触发更新;

    • 与写操作相关的方法:add、set、delete、clear。

返回迭代器对象的方法有:
  1. const iteratorMethods = [
  2. 'keys',
  3. 'values',
  4. 'entries',
  5. Symbol.iterator,
  6. ] as const
复制代码
其中Symbol.iterator是为了实现for of遍历必须实现的接口,在JavaScript中的所有可迭代对象都要实现这个接口。
正式开始阅读代码
这个文件的代码结构和baseHandlers不太一样,这个文件是先分别实现对get、set、has、size等操作的拦截,然后再整合成一个getter返回。
根据是否是shallow和readonly分别导出了四种handler:

  • mutableCollectionHandlers
  • shallowCollectionHandlers
  • readonlyCollectionHandlers
  • shallowReadonlyCollectionHandlers
export
  1. export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  2.   get: /*#__PURE__*/ createInstrumentationGetter(false, false),
  3. }
  4. export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
  5.   get: /*#__PURE__*/ createInstrumentationGetter(false, true),
  6. }
  7. export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
  8.   get: /*#__PURE__*/ createInstrumentationGetter(true, false),
  9. }
  10. export const shallowReadonlyCollectionHandlers: ProxyHandler<CollectionTypes> =
  11.   {
  12.     get: /*#__PURE__*/ createInstrumentationGetter(true, true),
  13.   }
复制代码
可以看到这些Handlers都是通过createInstrumentationGetter来返回getter,接下来看看createInstrumentationGetter内部是如何实现的。
createInstrumentationGetter

源码:(分段解析在下面)
  1. function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  2.   // 根据是否是只读和是否是浅层响应式来选择不同的处理函数集
  3.   const instrumentations = shallow
  4.     ? isReadonly
  5.       ? shallowReadonlyInstrumentations // 浅层只读的处理函数集
  6.       : shallowInstrumentations // 浅层的处理函数集
  7.     : isReadonly
  8.       ? readonlyInstrumentations // 只读的处理函数集
  9.       : mutableInstrumentations // 可变(非只读)的处理函数集
  10.   // 返回一个自定义的getter函数,用于处理特定的键
  11.   return (
  12.     target: CollectionTypes, // 目标集合类型
  13.     key: string | symbol, // 被访问的键
  14.     receiver: CollectionTypes, // 代理或包装过的集合
  15.   ) => {
  16.     // 检查特殊标志键
  17.     if (key === ReactiveFlags.IS_REACTIVE) {
  18.       // 如果不是只读的,返回true表示是响应式的
  19.       return !isReadonly
  20.     } else if (key === ReactiveFlags.IS_READONLY) {
  21.       // 如果是只读的,返回true表示是只读的
  22.       return isReadonly
  23.     } else if (key === ReactiveFlags.RAW) {
  24.       // 返回原始的目标集合
  25.       return target
  26.     }
  27.     // 使用Reflect.get来获取值
  28.     // 如果instrumentations有这个键,并且这个键在目标集合中,则从instrumentations获取
  29.     // 否则直接从目标集合获取
  30.     return Reflect.get(
  31.       hasOwn(instrumentations, key) && key in target
  32.         ? instrumentations
  33.         : target,
  34.       key,
  35.       receiver,
  36.     )
  37.   }
  38. }
复制代码
分段解读代码
  1. // 根据是否是只读和是否是浅层响应式来选择不同的处理函数集
  2.   const instrumentations = shallow
  3.     ? isReadonly
  4.       ? shallowReadonlyInstrumentations // 浅层只读的处理函数集
  5.       : shallowInstrumentations // 浅层的处理函数集
  6.     : isReadonly
  7.       ? readonlyInstrumentations // 只读的处理函数集
  8.       : mutableInstrumentations // 可变(非只读)的处理函数集
复制代码
这个函数根据isReadonly和isShallow选择了不同的函数集,函数集里的函数是特殊处理过的,目的是为了使这些实例方法可以适应Vue的响应式系统。
  1. // 检查特殊标志键
  2.     if (key === ReactiveFlags.IS_REACTIVE) {
  3.       // 如果不是只读的,返回true表示是响应式的
  4.       return !isReadonly
  5.     } else if (key === ReactiveFlags.IS_READONLY) {
  6.       // 如果是只读的,返回true表示是只读的
  7.       return isReadonly
  8.     } else if (key === ReactiveFlags.RAW) {
  9.       // 返回原始的目标集合
  10.       return target
  11.     }
复制代码
对于Vue内部特有的key,比如ReactiveFlags,返回特定的内容。这些ReactiveFlags并不存在于对象上,只是在getter做拦截并返回。
  1. // 使用Reflect.get来获取值
  2.     // 如果instrumentations有这个键,并且这个键在目标集合中,则从instrumentations获取
  3.     // 否则直接从目标集合获取
  4.     return Reflect.get(
  5.       hasOwn(instrumentations, key) && key in target
  6.         ? instrumentations
  7.         : target,
  8.       key,
  9.       receiver,
  10.     )
复制代码
最后,使用Reflect.get方法执行对key的访问并返回。这个时候会通过hasOwn(instrumentations, key)检查访问key是否在生成的函数集里:

  • 如果存在,那么应该应用特殊处理过的函数集里的函数;
  • 如果不存在,那么就用target身上原始的方法。
createInstrumentations

四种不同的函数集由createInstrumentations函数创建并返回。
  1. const [
  2.   mutableInstrumentations,
  3.   readonlyInstrumentations,
  4.   shallowInstrumentations,
  5.   shallowReadonlyInstrumentations,
  6. ] = /* #__PURE__*/ createInstrumentations()
复制代码
接下来是craeteInstrumentations的实现,是一段很长的代码:
这里只展示了mutableInstrucmentations的函数集,其它三个大同小异,其中的各种零碎的get、size、has...方法的处理在后文介绍。
  1. function createInstrumentations() {
  2.   // 定义可变(非只读)的响应式处理函数集
  3.   const mutableInstrumentations: Instrumentations = {
  4.     get(this: MapTypes, key: unknown) {
  5.       // 获取Map中的值,默认不是只读且不是浅层
  6.       return get(this, key)
  7.     },
  8.     get size() {
  9.       // 获取集合的大小
  10.       return size(this as unknown as IterableCollections)
  11.     },
  12.     has, // 检查集合中是否存在特定的值
  13.     add, // 向集合中添加元素
  14.     set, // 设置Map中的键值对
  15.     delete: deleteEntry, // 从集合中删除元素
  16.     clear, // 清空集合
  17.     forEach: createForEach(false, false), // 遍历集合的元素
  18.   }
  19.   // 定义浅层的响应式处理函数集
  20.   const shallowInstrumentations: Instrumentations = {
  21.     ...
  22.   }
  23.   // 定义只读的响应式处理函数集
  24.   const readonlyInstrumentations: Instrumentations = {
  25.     ...
  26.   }
  27.   // 定义浅层只读的响应式处理函数集
  28.   const shallowReadonlyInstrumentations: Instrumentations = {
  29.     ...
  30.   }
  31.   // 定义迭代器方法列表
  32.   const iteratorMethods = [
  33.     'keys',
  34.     'values',
  35.     'entries',
  36.     Symbol.iterator,
  37.   ] as const
  38.   // 为每个迭代器方法添加对应的响应式处理函数
  39.   iteratorMethods.forEach(method => {
  40.     mutableInstrumentations[method] = createIterableMethod(method, false, false)
  41.     readonlyInstrumentations[method] = createIterableMethod(method, true, false)
  42.     shallowInstrumentations[method] = createIterableMethod(method, false, true)
  43.     shallowReadonlyInstrumentations[method] = createIterableMethod(
  44.       method,
  45.       true,
  46.       true,
  47.     )
  48.   })
  49.   // 返回包含所有处理函数集的数组
  50.   return [
  51.     mutableInstrumentations, // 可变(非只读)的处理函数集
  52.     readonlyInstrumentations, // 只读的处理函数集
  53.     shallowInstrumentations, // 浅层的处理函数集
  54.     shallowReadonlyInstrumentations, // 浅层只读的处理函数集
  55.   ]
  56. }
复制代码
注意到迭代器方法也都做了特殊处理,这是因为迭代器方法返回迭代器对象,而不是操作对象本身,无法被Proxy拦截,故无法追踪依赖。
这里使用了createIterableMethod创建能够适配响应式的版本。
createIterableMethod

返回迭代器对象的几个需要处理的方法分别是:

  • keys
  • values
  • entries
  • Symbol.iterator
前三个是string类型传入,最后一个是symbol类型传入。
源码
  1. function createIterableMethod(
  2.   method: string | symbol, // 迭代器方法名,可以是字符串或符号
  3.   isReadonly: boolean, // 是否为只读的迭代器
  4.   isShallow: boolean, // 是否为浅层迭代器
  5. ) {
  6.   // 返回一个自定义的迭代器方法
  7.   return function (
  8.     this: IterableCollections, // 当前的集合对象
  9.     ...args: unknown[] // 方法调用的参数
  10.   ): Iterable<unknown> & Iterator<unknown> {
  11.     // 获取原始的集合对象
  12.     const target = (this as any)[ReactiveFlags.RAW]
  13.     const rawTarget = toRaw(target)
  14.     const targetIsMap = isMap(rawTarget) // 判断目标是否为Map类型
  15.     const isPair =
  16.       method === 'entries' || (method === Symbol.iterator && targetIsMap) // 判断是否为键值对迭代
  17.     const isKeyOnly = method === 'keys' && targetIsMap // 判断是否为仅键迭代
  18.     const innerIterator = target[method](...args) // 调用目标集合对象的迭代器方法
  19.     const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive // 获取包装函数
  20.     // 如果不是只读的,追踪迭代操作
  21.     !isReadonly &&
  22.       track(
  23.         rawTarget, // 追踪原始集合对象
  24.         TrackOpTypes.ITERATE, // 追踪操作类型为迭代
  25.         isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY, // 特殊标记,用于区分键迭代和普通迭代
  26.       )
  27.     // 返回一个包装过的迭代器,它返回包装过的值
  28.     return {
  29.       // 实现迭代器
  30.       next() {
  31.         const { value, done } = innerIterator.next() // 调用内部迭代器的next方法
  32.         return done
  33.           ? { value, done } // 如果迭代完成,返回当前值和完成标志
  34.           : {
  35.               value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), // 如果迭代未完成,返回包装过的值
  36.               done, // 完成标志保持不变
  37.             }
  38.       },
  39.       // 实现可迭代协议
  40.       [Symbol.iterator]() {
  41.         return this
  42.       },
  43.     }
  44.   }
  45. }
复制代码
Vue3在处理Map和Set的时候并没有分开处理,而是一起处理了,因为它们有许多名字相同的方法,分开处理可能会导致代码更乱。
对于entries的输出,也就是[key, value]格式的遍历,通过简单的判断处理了:
  1. const isPair =
  2.       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的类型。
源码
  1. function get(
  2.   target: MapTypes,  // 目标对象,类型是 MapTypes
  3.   key: unknown,  // 要获取值的键,类型是 unknown
  4.   isReadonly = false,  // 是否只读,默认值为 false
  5.   isShallow = false  // 是否浅层响应,默认值为 false
  6. ) {
  7.   // 确保如果 target 是响应式对象,操作的是它的原始对象
  8.   target = (target as any)[ReactiveFlags.RAW]
  9.   // 获取 target 的原始对象
  10.   const rawTarget = toRaw(target)
  11.   // 获取 key 的原始值
  12.   const rawKey = toRaw(key)
  13.   
  14.   if (!isReadonly) {
  15.     // 如果 key 与 rawKey 不同(即 key 是响应式对象),跟踪对 key 的访问
  16.     if (hasChanged(key, rawKey)) {
  17.       track(rawTarget, TrackOpTypes.GET, key)
  18.     }
  19.     // 跟踪对 rawKey 的访问
  20.     track(rawTarget, TrackOpTypes.GET, rawKey)
  21.   }
  22.   // 获取 target 原型上的 has 方法
  23.   const { has } = getProto(rawTarget)
  24.   // 根据 isShallow 和 isReadonly 选择对应的包装函数
  25.   const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  26.   
  27.   // 如果原始对象上存在 key,则返回包装后的值
  28.   if (has.call(rawTarget, key)) {
  29.     return wrap(target.get(key))
  30.   // 如果原始对象上存在 rawKey,则返回包装后的值
  31.   } else if (has.call(rawTarget, rawKey)) {
  32.     return wrap(target.get(rawKey))
  33.   // 如果 target 不是原始对象,则调用 target.get(key) 进行跟踪
  34.   } else if (target !== rawTarget) {
  35.     // 确保在只读的响应式 Map 中,嵌套的响应式 Map 也能进行依赖跟踪
  36.     target.get(key)
  37.   }
  38. }
复制代码
最后为什么还要加一个判断target!==rawTarget?
这个判断和一个bug有关:readonly() breaks reactivity of Map · Issue #3602 · vuejs/core (github.com)
对应的fix:fix(reactivity): fix the tracking when readonly + reactive is used for Map by HcySunYang · Pull Request #3604 · vuejs/core (github.com)
背景
在 Vue3 的响应式系统中,readonly 和 reactive 组合使用时可能会出现一些问题,特别是在处理嵌套结构时。例如,当你有一个 readonly 包装的 reactive Map,并试图在这个 Map 中获取一个值,如果不进行额外处理,可能会导致嵌套的响应式 Map 无法正确进行依赖跟踪。
示例代码
  1. const reactiveMap = reactive(new Map([['key', new Map([['nestedKey', 'value']])]]));
  2. const readonlyMap = readonly(reactiveMap);
  3. // 获取嵌套的 Map
  4. const nestedMap = readonlyMap.get('key');
  5. // 尝试获取嵌套 Map 的值
  6. 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)
  1. export function toRaw<T>(observed: T): T {
  2. // 尝试获取raw对象
  3. const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  4. // 如果存在raw对象,则递归调用;如果不存在,则表示当前的observed已经是原始对象
  5. return raw ? toRaw(raw) : observed
  6. }
复制代码
可以看到如果传入的对象如果有ReactiveFlags.RAW这个key,就认为它是被Vue包装过的对象,因为只有被reactive、readonly等API包装过的对象会被Vue添加上ReactiveFlags.RAW属性,记录着原始对象的引用。
这里需要递归调用是因为对象可能被多层包装,比如readonly(reactive({}))。
回到Map的get方法的最后处理
  1. if (target !== rawTarget) {
  2. // 确保在只读的响应式 Map 中,嵌套的响应式 Map 也能进行依赖跟踪
  3. target.get(key)
  4. }
复制代码

  • 如果target === rawTarget,则target是原始对象;
  • 如果target!==rawTarget,则target是包装过的对象,可能是reactive包装过的响应式对象,也可能是readonly包装过的只读对象;
    这里或许可以再优化?如果是只读对象,就不追踪依赖了。
set

Map的key可能是原始值也可能是响应式对象,这里需要做类型判断,并且对原始key和响应式key都做判断。
在开发环境下如果存在同一个原始对象的两种类型的key,会输出警告。
因为这种不规范的写法会保存两份键值对,内容可能不一致。
源码
  1. function set(this: MapTypes, key: unknown, value: unknown, _isShallow = false) {
  2.   // 如果值不是浅层的且不是只读的,则获取其原始值
  3.   if (!_isShallow && !isShallow(value) && !isReadonly(value)) {
  4.     value = toRaw(value)
  5.   }
  6.   // 获取目标对象的原始对象
  7.   const target = toRaw(this)
  8.   const { has, get } = getProto(target)
  9.   // 检查目标对象是否已经存在该键
  10.   let hadKey = has.call(target, key)
  11.   if (!hadKey) {
  12.     // 如果不存在,尝试使用原始键进行再次检查
  13.     key = toRaw(key)
  14.     hadKey = has.call(target, key)
  15.   } else if (__DEV__) {
  16.     // 在开发环境中,检查键的类型是否一致
  17.     checkIdentityKeys(target, has, key)
  18.   }
  19.   // 获取旧值
  20.   const oldValue = get.call(target, key)
  21.   // 设置新值
  22.   target.set(key, value)
  23.   // 触发依赖追踪
  24.   if (!hadKey) {
  25.     // 如果键之前不存在,触发添加操作的依赖
  26.     trigger(target, TriggerOpTypes.ADD, key, value)
  27.   } else if (hasChanged(value, oldValue)) {
  28.     // 如果键之前存在且值发生了变化,触发设置操作的依赖
  29.     trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  30.   }
  31.   // 返回 this 以支持链式调用
  32.   return this
  33. }
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册