各位观众,掌声鼓励一下!今天咱们来聊聊 Vue 3 源码里的一个“神秘组织”—— collectionHandlers
。 别看名字高大上,其实它就是个“集合管家”,专门负责照顾 Map
、Set
这些集合类型的响应性。
开场白:集合类型,响应性的“后花园”
在Vue的世界里,数据驱动视图更新是核心理念。对于普通的对象属性,实现响应性相对简单,无非就是监听 get
和 set
操作。但是,Map
和 Set
这些集合类型,它们的操作可不是简单的属性赋值,而是 add
、delete
、clear
等方法调用。
如果还是用监听 get
和 set
的老办法,那就抓瞎了。想象一下,你往 Map
里 add
了一个元素,Vue 却毫无反应,视图纹丝不动,那还玩啥?collectionHandlers
的存在,就是为了解决这个问题,让集合类型的操作也能触发视图更新。
第一幕:collectionHandlers
的真面目
collectionHandlers
本质上是一个对象,里面定义了一系列“拦截器”,专门拦截 Map
、Set
等集合类型的方法调用。这些“拦截器”会在方法执行前后偷偷地“搞事情”,触发响应性更新。
// packages/reactivity/src/collectionHandlers.ts
import {
mutableCollectionInstrumentations,
readonlyCollectionInstrumentations,
shallowCollectionInstrumentations
} from './collectionUtils'
import {
mutableInstrumentations,
readonlyInstrumentations,
shallowInstrumentations
} from './baseHandlers'
import {
isReadonly,
isShallow,
targetMap,
track,
trigger,
ITERATE_KEY,
pauseTracking,
enableTracking
} from './reactive'
import { hasOwn, isObject } from '@vue/shared'
function createCollectionHandler(instrumentations: Record<string, Function>) {
return {
get(target: any, key: string | symbol, receiver: any) {
// 1. 拦截 has 方法
if (key === 'size') {
return Reflect.get(target, key, receiver)
}
// 2. 拦截其他方法
// - 如果 key 存在于 instrumentations 中,则返回 instrumentations 中定义的方法
// - 否则,返回原始方法
return instrumentations[key] || Reflect.get(target, key, receiver)
},
has(target: any, key: any) {
track(target, 'has')
return Reflect.has(target, key)
},
ownKeys(target: any) {
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
}
}
}
export const mutableCollectionHandlers: ProxyHandler<any> = createCollectionHandler(
mutableCollectionInstrumentations
)
export const readonlyCollectionHandlers: ProxyHandler<any> = createCollectionHandler(
readonlyCollectionInstrumentations
)
export const shallowReadonlyCollectionHandlers: ProxyHandler<any> = createCollectionHandler(
shallowCollectionInstrumentations
)
简单来说,collectionHandlers
针对 Map
、Set
等集合类型,重写了 get
、has
、ownKeys
这些方法。当访问集合类型的方法时,会先经过 collectionHandlers
的处理,然后再执行原始方法。
第二幕:mutableCollectionInstrumentations
,响应性的“魔法棒”
mutableCollectionInstrumentations
是一个对象,里面定义了 add
、delete
、clear
等方法的“拦截器”。这些“拦截器”会在方法执行前后调用 track
和 trigger
函数,从而实现响应性更新。
// packages/reactivity/src/collectionUtils.ts
import { track, trigger, ITERATE_KEY, pauseTracking, enableTracking } from './reactive'
import { isReadonly, isShallow } from './reactive'
import { hasOwn, isObject } from '@vue/shared'
function createInstrumentations() {
return {
get(key: any) {
const target = this[RAW]
const isReadonly = this[IS_READONLY]
if (_isReadonly) {
track(target, key)
} else if (isShallow(this)) {
// shallow reactive collection doesn't track
return target.get(key)
} else {
track(target, key)
}
const res = target.get(key)
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
},
has(key: any) {
const target = this[RAW]
track(target, key)
return target.has(key)
},
add(value: any) {
const target = this[RAW]
const result = target.add(value)
trigger(target, value)
return result
},
set(key: any, value: any) {
const target = this[RAW]
const result = target.set(key, value)
trigger(target, key)
return result
},
delete(key: any) {
const target = this[RAW]
const hadKey = target.has(key)
const result = target.delete(key)
if (hadKey) {
trigger(target, 'delete')
}
return result
},
clear() {
const target = this[RAW]
const hadItems = target.size !== 0
const result = target.clear()
if (hadItems) {
trigger(target, ITERATE_KEY)
}
return result
},
forEach(callback: Function, thisArg?: any) {
const target = this[RAW]
const isReadonly = this[IS_READONLY]
pauseTracking()
try {
target.forEach((value: any, key: any) => {
// we have to double check because the target may self-mutate during
// iteration.
track(target, key)
value = isReadonly ? readonly(value) : reactive(value)
callback.call(thisArg, value, key, this)
})
} finally {
enableTracking()
}
},
*keys() {
const target = this[RAW]
const iterator = target.keys()
let current
return {
next() {
track(target, current)
current = iterator.next()
return current
},
[Symbol.iterator]() {
return this
}
}
},
*values() {
const target = this[RAW]
const iterator = target.values()
let current
return {
next() {
track(target, current)
current = iterator.next()
return current
},
[Symbol.iterator]() {
return this
}
}
},
*entries() {
const target = this[RAW]
const iterator = target.entries()
let current
return {
next() {
track(target, current)
current = iterator.next()
return current
},
[Symbol.iterator]() {
return this
}
}
},
*[Symbol.iterator]() {
return this.entries()
},
get size() {
const target = this[RAW]
track(target, ITERATE_KEY)
return Reflect.get(target, 'size', this)
}
}
}
export const mutableCollectionInstrumentations: Record<string, Function> =
createInstrumentations()
export const readonlyCollectionInstrumentations: Record<string, Function> =
createInstrumentations()
export const shallowCollectionInstrumentations: Record<string, Function> =
createInstrumentations()
咱们以 add
方法为例,看看它是怎么“搞事情”的:
add(value: any) {
const target = this[RAW] // 获取原始的 Map/Set 对象
const result = target.add(value); // 执行原始的 add 操作
trigger(target, value); // 触发响应性更新
return result;
}
可以看到,add
方法先执行原始的 add
操作,然后调用 trigger
函数,通知 Vue 有数据发生了变化。Vue 就会重新渲染视图,从而实现响应性更新。
第三幕:track
和 trigger
,响应性的“发动机”
track
和 trigger
是 Vue 3 响应式系统的核心函数。track
函数用于收集依赖,trigger
函数用于触发更新。
-
track(target, key)
: 当读取响应式数据时,track
函数会被调用,它会将当前正在执行的副作用函数(比如渲染函数)与该数据关联起来,建立依赖关系。 -
trigger(target, key)
: 当响应式数据发生变化时,trigger
函数会被调用,它会找到所有与该数据关联的副作用函数,并执行它们,从而触发视图更新。
在 collectionHandlers
中,track
和 trigger
函数被巧妙地运用,实现了集合类型的响应性更新。
举个栗子:Map
的响应性
假设我们有一个 Map
对象:
const map = reactive(new Map());
现在,我们往 map
里 add
一个元素:
map.add('name', 'Vue');
这个 add
操作会触发 mutableCollectionInstrumentations.add
方法的执行。add
方法会先执行原始的 add
操作,然后调用 trigger(map, 'name')
。trigger
函数会找到所有依赖于 map
的副作用函数,并执行它们,从而触发视图更新。
表格总结:collectionHandlers
的“拦截器”
方法 | 拦截器 | 作用 |
---|---|---|
add |
mutableCollectionInstrumentations.add |
执行原始 add 操作,然后调用 trigger 函数,触发响应性更新。 |
delete |
mutableCollectionInstrumentations.delete |
执行原始 delete 操作,如果删除成功,则调用 trigger 函数,触发响应性更新。 |
clear |
mutableCollectionInstrumentations.clear |
执行原始 clear 操作,如果清空成功,则调用 trigger 函数,触发响应性更新。 |
get |
mutableCollectionInstrumentations.get |
执行原始 get 操作,然后调用 track 函数,收集依赖。 |
has |
mutableCollectionInstrumentations.has |
执行原始 has 操作,然后调用 track 函数,收集依赖。 |
set |
mutableCollectionInstrumentations.set |
执行原始 set 操作,然后调用 trigger 函数,触发响应性更新。 |
forEach |
mutableCollectionInstrumentations.forEach |
执行原始 forEach 操作,并在每次迭代时调用 track 函数,收集依赖。 |
keys |
mutableCollectionInstrumentations.keys |
执行原始 keys 操作,并在每次迭代时调用 track 函数,收集依赖。 |
values |
mutableCollectionInstrumentations.values |
执行原始 values 操作,并在每次迭代时调用 track 函数,收集依赖。 |
entries |
mutableCollectionInstrumentations.entries |
执行原始 entries 操作,并在每次迭代时调用 track 函数,收集依赖。 |
size |
mutableCollectionInstrumentations.size |
读取原始 size 属性,然后调用 track 函数,收集依赖。 |
代码示例:一个简单的 Map
响应式组件
<template>
<div>
<p>Map Size: {{ map.size }}</p>
<ul>
<li v-for="(value, key) in map" :key="key">{{ key }}: {{ value }}</li>
</ul>
<button @click="addItem">Add Item</button>
<button @click="deleteItem">Delete Item</button>
<button @click="clearMap">Clear Map</button>
</div>
</template>
<script>
import { reactive } from 'vue';
export default {
setup() {
const map = reactive(new Map());
map.set('name', 'Vue');
map.set('version', '3');
const addItem = () => {
map.set(Date.now(), 'New Item');
};
const deleteItem = () => {
if (map.size > 0) {
const key = map.keys().next().value;
map.delete(key);
}
};
const clearMap = () => {
map.clear();
};
return {
map,
addItem,
deleteItem,
clearMap,
};
},
};
</script>
在这个组件中,我们使用 reactive
函数将 Map
对象转换为响应式数据。当 Map
对象发生变化时,组件会自动更新视图。这都要归功于 collectionHandlers
的“幕后工作”。
深入思考:ITERATE_KEY
的作用
在 collectionHandlers
中,ITERATE_KEY
是一个特殊的 key,用于表示集合类型的迭代。当集合类型发生 add
、delete
、clear
等操作时,trigger(target, ITERATE_KEY)
会被调用,通知所有依赖于集合类型迭代的副作用函数进行更新。
例如,在上面的组件中,v-for="(value, key) in map"
依赖于 map
的迭代。当 map
发生 add
、delete
、clear
等操作时,trigger(map, ITERATE_KEY)
会被调用,v-for
会重新渲染,从而更新视图。
PauseTracking
和EnableTracking
在forEach
的实现里,你可能会有疑问,为什么会有pauseTracking
和enableTracking
?
这是因为在 forEach
循环内部,我们可能会读取到 Map
中的值,而读取操作会触发 track
函数,建立依赖关系。但是,我们并不希望在 forEach
循环内部建立额外的依赖关系,因为这可能会导致不必要的更新。
因此,我们在 forEach
循环开始前调用 pauseTracking
函数,暂停依赖收集。在 forEach
循环结束后,我们调用 enableTracking
函数,恢复依赖收集。这样,我们就可以避免在 forEach
循环内部建立额外的依赖关系。
总结:collectionHandlers
,响应性的“守护者”
collectionHandlers
是 Vue 3 响应式系统的重要组成部分。它通过拦截 Map
、Set
等集合类型的方法调用,实现了集合类型的响应性更新。有了 collectionHandlers
,我们就可以放心地使用集合类型,而不用担心响应性问题。
希望今天的讲座能够帮助大家更好地理解 Vue 3 源码中的 collectionHandlers
。 咱们下期再见!