Vue 3 响应式集合类型:一场“监听风云”的实况转播
各位观众,各位朋友,晚上好!欢迎来到“Vue源码一日游”特别节目。我是今天的导游,名叫……呃,就叫我“源码君”吧!
今天,我们要深入Vue 3的响应式宝库,扒一扒那些“不太安分”的集合类型:Map
、Set
。 它们可不是简单的容器,在Vue 3的魔法加持下,它们的一举一动都会被密切关注,任何风吹草动都会触发视图的更新。
准备好了吗?让我们开始这场“监听风云”的实况转播!
前戏:响应式的基本原理回顾
在深入集合类型之前,我们先快速回顾一下Vue 3响应式的核心机制。Vue 3 使用 Proxy
对数据进行拦截,通过 track
收集依赖,通过 trigger
触发更新。
简单来说:
- Proxy 上岗: Vue 3 会用
Proxy
包装你的数据对象,创建一个代理对象。 - Track 侦查: 当你在模板中使用某个响应式数据时,Vue 3 会通过
track
函数,把当前组件的effect
函数(也就是用于更新视图的函数)和这个数据关联起来,建立“依赖关系”。 - Trigger 告警: 当你修改响应式数据时,Vue 3 会通过
trigger
函数,找到所有依赖于这个数据的effect
函数,并执行它们,从而更新视图。
这就像一个警察局(Proxy),警察(track)记录谁经常出入哪个地方(访问哪个响应式数据),一旦这个地方发生了什么事(数据被修改),警察就会通知那些经常出入的人(执行effect函数),让他们采取行动(更新视图)。
正戏:集合类型,你们的“特殊待遇”来了!
普通的属性访问,Proxy
拦截 get
和 set
就够了。但是 Map
和 Set
这些集合类型,它们的操作可不仅仅是简单的赋值和取值,还有 add
、delete
、clear
等等。为了应对这些“花样百出”的操作,Vue 3 特别定制了一套方案,也就是我们今天要重点讲解的 collectionHandlers
。
collectionHandlers
本质上是一个对象,它定义了针对不同集合类型(Map
、Set
、WeakMap
、WeakSet
)的各种操作的拦截器。 让我们来看一段关键的源码(简化版):
// packages/reactivity/src/collectionHandlers.ts
import { mutableInstrumentations } from './mutableCollection'
import { trackOpBit } from './operations'
import { TrackOpTypes, TriggerOpTypes } from './constants'
import {
isReadonly,
isShallow,
readonly,
shallowReadonly
} from './reactive'
import {
iteratorKey,
ITERATE_KEY,
MAP_KEY_ITERATE_KEY,
ReactiveEffect
} from './reactiveEffect'
import { triggerRef } from './ref'
import {
hasOwn,
isArray,
isIntegerKey,
isMap,
isSymbol
} from '@vue/shared'
import { track, trigger } from './effect'
function createGetter(isReadonly = false, shallow = false) {
return function get(target: any, key: string | symbol, receiver: object) {
const isReadonly = false;
const isShallow = false;
// ... 省略部分代码
// collection type (Map, Set, ...)
if (key === ReactiveFlags.RAW && receiver === reactiveMap.get(target)) {
return target
}
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (
key === ReactiveFlags.IS_SHALLOW
) {
return isShallow
} else if (
key === ReactiveFlags.IS_PROXY
) {
return true
}
const targetIsArray = isArray(target)
// handle array access
if (targetIsArray && isIntegerKey(key)) {
// make sure to track the length as well
track(target, TrackOpTypes.GET, key)
return Reflect.get(target, key, receiver)
}
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) ? builtInSymbols.has(key) : false) {
return res
}
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// Convert returned value into a reactive data structure.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
function createSetter(shallow = false) {
return function set(
target: any,
key: string | symbol,
value: any,
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (!shallow) {
value = toRaw(value)
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
}
const hadKey = isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of the `receiver`
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
export const mutableCollectionHandlers: ProxyHandler<any> = {
get: /*#__PURE__*/ createGetter(),
set: /*#__PURE__*/ createSetter(),
has: (target: object, key: string | symbol): boolean => {
const result = Reflect.has(target, key)
track(target, TrackOpTypes.HAS, key)
return result
},
ownKeys: (target: object): (string | symbol)[] => {
track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.ownKeys(target)
}
}
export const readonlyCollectionHandlers: ProxyHandler<any> = {
get: /*#__PURE__*/ createGetter(true),
set(target: any, key: string | symbol): boolean {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
},
deleteProperty(target: object, key: string | symbol): boolean {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
},
has: (target: object, key: string | symbol): boolean => {
const result = Reflect.has(target, key)
track(target, TrackOpTypes.HAS, key)
return result
},
ownKeys: (target: object): (string | symbol)[] => {
track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.ownKeys(target)
}
}
function getReadonly(isReadonly = false, shallow = false) {
return function get(target: any, key: string | symbol, receiver: object) {
// ... 省略部分代码
// collection type (Map, Set, ...)
if (key === ReactiveFlags.RAW && receiver === readonlyMap.get(target)) {
return target
}
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (
key === ReactiveFlags.IS_SHALLOW
) {
return isShallow
} else if (
key === ReactiveFlags.IS_PROXY
) {
return true
}
const targetIsArray = isArray(target)
// handle array access
if (targetIsArray && isIntegerKey(key)) {
// make sure to track the length as well
track(target, TrackOpTypes.GET, key)
return Reflect.get(target, key, receiver)
}
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) ? builtInSymbols.has(key) : false) {
return res
}
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// Convert returned value into a reactive data structure.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
const shallowReadonlyGet = /*#__PURE__*/ getReadonly(true, true);
export const shallowReadonlyCollectionHandlers: ProxyHandler<any> = {
get: shallowReadonlyGet,
set(target: any, key: string | symbol): boolean {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
},
deleteProperty(target: object, key: string | symbol): boolean {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
},
has: (target: object, key: string | symbol): boolean => {
const result = Reflect.has(target, key)
track(target, TrackOpTypes.HAS, key)
return result
},
ownKeys: (target: object): (string | symbol)[] => {
track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.ownKeys(target)
}
}
export const readonlyMap = new WeakMap<object, any>()
export const mutableMap: WeakMap<object, any> = new WeakMap<object, any>()
(源码位置:packages/reactivity/src/collectionHandlers.ts
)
mutableCollectionHandlers
是 Proxy
的一个配置项,包含了 get
、set
、has
、ownKeys
方法。
重点来了! collectionHandlers
真正发挥威力的地方,在于它与 mutableInstrumentations
的配合。mutableInstrumentations
是一个对象,里面存储了对 Map
、Set
等集合类型原生方法的“增强版”。
// packages/reactivity/src/mutableCollection.ts
import { isReadonly, isShallow, reactive, readonly } from './reactive'
import { track, trigger } from './effect'
import {
TrackOpTypes,
TriggerOpTypes
} from './constants'
import {
hasOwn,
hasChanged,
isMap,
isSet
} from '@vue/shared'
const canInstrument = (val: object | undefined) => !!val && '__v_isReactive' in val
export const mutableInstrumentations: Record<string, Function> = {
get(this: Map<any, any>, key: any) {
const target = this[RAW]
const hadKey = has(target, key)
track(
target,
TrackOpTypes.GET,
key
)
if (hadKey) {
return isReadonly(target)
? readonly(target.get(key))
: isShallow(target)
? target.get(key)
: reactive(target.get(key))
}
},
has(this: Map<any, any> | Set<any>, key: any) {
const target = this[RAW]
track(target, TrackOpTypes.HAS, key)
return target.has(key)
},
add(this: Set<any>, value: any) {
const target = this[RAW]
const hadKey = target.has(value)
const result = target.add(value)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, value)
}
return result
},
set(this: Map<any, any>, key: any, value: any) {
const target = this[RAW]
const hadKey = target.has(key)
const oldValue = target.get(key)
const rawValue = value //toRaw(value)
target.set(key, rawValue)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key)
} else if (hasChanged(rawValue, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value)
}
return this
},
delete(this: Map<any, any> | Set<any>, key: any) {
const target = this[RAW]
const hadKey = target.has(key)
const result = target.delete(key)
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key)
}
return result
},
clear(this: Map<any, any> | Set<any>) {
const target = this[RAW]
const hadItems = target.size !== 0
const result = target.clear()
if (hadItems) {
trigger(
target,
TriggerOpTypes.CLEAR,
undefined
)
}
return result
},
forEach(this: Map<any, any>, callback: Function, thisArg?: any) {
const target = this[RAW]
const wrap = (val: any) => isReadonly(target)
? readonly(val)
: isShallow(target)
? val
: reactive(val)
target.forEach((value: any, key: any) => {
// important: make sure isReadonly(value) and isShallow(value) is
// checked for the original value.
callback.call(thisArg, wrap(value), wrap(key), this)
})
},
keys(this: Map<any, any>, iterationType: ITERATION_KEY = ITERATE_KEY) {
const target = this[RAW]
const isReadonly = isReadonly(target)
const isShallow = isShallow(target)
const iterator = target.keys()
return {
next() {
const { value, done } = iterator.next()
if (done) {
return { value, done }
}
return {
value: wrap(value),
done: false
}
},
[Symbol.iterator]() {
return this
}
}
},
values(this: Map<any, any>, iterationType: ITERATION_KEY = ITERATE_KEY) {
const target = this[RAW]
const isReadonly = isReadonly(target)
const isShallow = isShallow(target)
const iterator = target.values()
return {
next() {
const { value, done } = iterator.next()
if (done) {
return { value, done }
}
return {
value: wrap(value),
done: false
}
},
[Symbol.iterator]() {
return this
}
}
},
entries(this: Map<any, any>, iterationType: ITERATION_KEY = ITERATE_KEY) {
const target = this[RAW]
const isReadonly = isReadonly(target)
const isShallow = isShallow(target)
const iterator = target.entries()
return {
next() {
const { value, done } = iterator.next()
if (done) {
return { value, done }
}
return {
value: [wrap(value[0]), wrap(value[1])],
done: false
}
},
[Symbol.iterator]() {
return this
}
}
},
[Symbol.iterator](this: Map<any, any>, iterationType: ITERATION_KEY = ITERATE_KEY) {
return this.entries()
}
}
function wrap(value: any) {
return isReadonly(value)
? readonly(value)
: isShallow(value)
? value
: reactive(value)
}
; (['keys', 'values', 'entries', Symbol.iterator] as const).forEach(method => {
mutableInstrumentations[method] = function (this: Map<any, any>, iterationType: ITERATION_KEY = ITERATE_KEY) {
const target = this[RAW]
track(target, TrackOpTypes.ITERATE, iterationType)
// instead of returning the native iterator directly, return a wrapped one.
// this makes sure all entries/keys/values returned from the iterator are
// reactive
return mutableInstrumentations[method].call(target, iterationType)
}
})
; (['add', 'delete', 'clear', 'set'] as const).forEach(method => {
mutableInstrumentations[method] = function (this: Map<any, any> | Set<any>, ...args: any[]) {
const target = this[RAW]
if (__DEV__) {
const proto = Object.getPrototypeOf(this)
if (proto.hasOwnProperty('__v_isReadonly')) {
console.warn(`Avoid mutating a readonly object value as it results in unexpected behavior.
Object: ${proto}`)
}
}
const result = target[method](...args)
return result
}
})
export {
canInstrument
}
(源码位置:packages/reactivity/src/mutableCollection.ts
)
现在,当我们使用 reactive
函数将一个 Map
或 Set
变成响应式对象时,Vue 3 会偷偷地把这个响应式对象的原型链上的对应方法替换成 mutableInstrumentations
里的“增强版”方法。
举个例子:
假设我们有以下代码:
import { reactive } from 'vue'
const myMap = reactive(new Map());
myMap.set('name', '源码君'); // 调用增强版的 set 方法
当我们调用 myMap.set('name', '源码君')
时,实际上调用的是 mutableInstrumentations.set
方法。 这个“增强版”的 set
方法做了什么呢?
- 获取原始对象: 首先,它会通过
this[RAW]
获取到原始的Map
对象(没有被Proxy
代理的那个)。 - 执行原生操作: 然后,它会调用原始
Map
对象的set
方法,真正地把键值对设置进去。 - 触发更新: 最关键的一步来了! 在完成原生操作后,它会调用
trigger
函数,通知所有依赖于这个Map
的组件进行更新。
add
、delete
、clear
等方法也是类似的流程,都是先执行原生操作,然后触发更新。
精彩瞬间回放:collectionHandlers
+ mutableInstrumentations
为了让大家更直观地理解,我们用一个表格来总结一下 collectionHandlers
和 mutableInstrumentations
是如何协同工作的:
操作类型 | collectionHandlers 职责 |
mutableInstrumentations 职责 |
---|---|---|
get |
拦截 get 操作,但实际的取值操作会被代理到 mutableInstrumentations.get |
1. 通过 this[RAW] 获取原始 Map 对象。 2. 调用原始 Map 对象的 get 方法。 3. 使用 track 函数收集依赖。 4. 如果值是对象,则递归地将其转换为响应式对象。 |
set |
拦截 set 操作,但实际的设置操作会被代理到 mutableInstrumentations.set |
1. 通过 this[RAW] 获取原始 Map 对象。 2. 调用原始 Map 对象的 set 方法。 3. 使用 trigger 函数触发更新。 |
add |
拦截 add 操作,但实际的添加操作会被代理到 mutableInstrumentations.add |
1. 通过 this[RAW] 获取原始 Set 对象。 2. 调用原始 Set 对象的 add 方法。 3. 使用 trigger 函数触发更新。 |
delete |
拦截 delete 操作,但实际的删除操作会被代理到 mutableInstrumentations.delete |
1. 通过 this[RAW] 获取原始 Map 或 Set 对象。 2. 调用原始 Map 或 Set 对象的 delete 方法。 3. 使用 trigger 函数触发更新。 |
clear |
拦截 clear 操作,但实际的清空操作会被代理到 mutableInstrumentations.clear |
1. 通过 this[RAW] 获取原始 Map 或 Set 对象。 2. 调用原始 Map 或 Set 对象的 clear 方法。 3. 使用 trigger 函数触发更新。 |
has |
拦截 has 操作,但实际的检查操作会被代理到 mutableInstrumentations.has ,同时 track 收集依赖 |
1. 通过 this[RAW] 获取原始 Map 或 Set 对象。 2. 调用原始 Map 或 Set 对象的 has 方法。 |
ownKeys |
拦截 ownKeys 操作, 同时 track 收集依赖 |
无特殊处理,直接 Reflect.ownKeys(target) |
幕后花絮:为什么要这么复杂?
你可能会问:为什么要搞得这么复杂?直接在 Proxy
的 set
拦截器里处理 Map
和 Set
的操作不行吗?
答案是:不行!
因为 Map
和 Set
的操作,例如 add
、delete
、clear
,都不是通过简单的属性赋值来实现的。 如果只拦截 set
操作,就无法监听到这些操作,也就无法实现响应式更新。
Vue 3 的这种方案,通过 collectionHandlers
和 mutableInstrumentations
的配合,实现了对 Map
和 Set
所有操作的全面监听,确保了响应式的完整性。
附加彩蛋:WeakMap
和 WeakSet
的响应式处理
WeakMap
和 WeakSet
与 Map
和 Set
略有不同。 它们是弱引用集合,这意味着它们不会阻止垃圾回收器回收键或值。
由于 WeakMap
和 WeakSet
的键是弱引用的,因此无法像 Map
和 Set
那样进行迭代。 这也意味着无法监听 WeakMap
和 WeakSet
的 size
属性的变化。
因此,Vue 3 对 WeakMap
和 WeakSet
的响应式处理相对简单,只拦截 get
、set
、has
、delete
等操作,而没有对 add
、clear
等操作进行特殊处理。 此外,由于 WeakMap
和 WeakSet
的键是对象,因此对值的响应式处理与普通对象类似。
总结陈词:监听风云,圆满落幕
今天,我们一起深入 Vue 3 源码,见证了 Map
和 Set
等集合类型在响应式世界里的“特殊待遇”。 通过 collectionHandlers
和 mutableInstrumentations
的巧妙配合,Vue 3 实现了对这些集合类型所有操作的全面监听,确保了响应式的完整性和准确性。
这场“监听风云”的实况转播到此结束。 感谢大家的收看,我们下期再见!
(源码君鞠躬退场)