各位靓仔靓女,晚上好!我是今天的主讲人,咱们今晚聊聊Vue 3 reactive里的那些“集合大佬”:Set、Map等等。
咳咳,开始之前先声明,今天咱不搞玄学,直接扒源码,争取用最通俗易懂的语言,把Vue 3 reactive处理集合类型的逻辑给各位安排明白。
开场白:为啥集合类型需要特别关照?
在Vue 3的世界里,reactive的核心任务就是把一个普通的JavaScript对象变成响应式的,一旦这个对象的属性发生改变,所有依赖于这个属性的视图或者计算属性都要跟着更新。
对于普通对象,这事儿很简单,直接用Proxy
拦截get
、set
等操作就完事了。但是,对于Set
、Map
这种集合类型,情况就复杂多了。
为啥呢?因为集合类型有自己的专属操作方法,比如add
、delete
、clear
,如果我们只拦截get
、set
,那这些集合方法的操作就绕过了我们的监听,导致视图无法更新。
举个栗子:
const reactiveSet = reactive(new Set());
reactiveSet.add(1); // 视图没更新!
所以,Vue 3必须对这些集合类型进行特殊处理,才能保证它们的响应式特性。
第一幕:mutableCollectionHandlers
——集合类型的“御用Handler”
Vue 3为了应对集合类型的特殊性,专门定义了一个mutableCollectionHandlers
对象,里面包含了处理Set
、Map
等可变集合类型的get
拦截器。
// packages/reactivity/src/reactive.ts
export const mutableCollectionHandlers: ProxyHandler<any> = {
get: /*#__PURE__*/ createCollectionGetter(),
set: mutableCollectionSetter,
has: /*#__PURE__*/ createCollectionGetter(readonly = false, shallow = false, isReadonly = false),
add: /*#__PURE__*/ createCollectionGetter(readonly = false, shallow = false, isReadonly = false),
delete: /*#__PURE__*/ createCollectionGetter(readonly = false, shallow = false, isReadonly = false),
clear: /*#__PURE__*/ createCollectionGetter(readonly = false, shallow = false, isReadonly = false),
forEach: /*#__PURE__*/ createCollectionGetter(readonly = false, shallow = false, isReadonly = false),
size: /*#__PURE__*/ createCollectionGetter(readonly = false, shallow = false, isReadonly = false),
//... 其他方法
};
可以看到,mutableCollectionHandlers
里定义了get
、set
、has
、add
、delete
、clear
、forEach
、size
等方法,这些方法都是为了拦截集合类型的各种操作。
第二幕:createCollectionGetter
——“Getter制造机”
createCollectionGetter
函数的作用是生成一个用于拦截集合类型get
操作的getter函数。它的核心逻辑是:
- 拦截集合类型的
get
操作。 - 判断
key
是否是集合类型的方法(比如add
、delete
)。 - 如果是集合类型的方法,则返回一个经过特殊处理的函数,这个函数会在执行集合方法前后触发依赖收集和触发更新。
- 如果不是集合类型的方法,则直接返回原始值。
// packages/reactivity/src/reactive.ts
function createCollectionGetter(readonly: boolean = false, shallow: boolean = false, isReadonly: boolean = false) {
return function get(target: any, key: string | symbol, receiver: any) {
// 1. 拦截集合类型的get操作。
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
} else if (key === ReactiveFlags.IS_READONLY) {
return readonly;
} else if (key === ReactiveFlags.RAW && receiver === reactiveMap.get(target)) {
return target;
}
const targetIsReadonly = isReadonly || readonly;
// target === Readonly + NOT Reactive
// result === Readonly
const self = targetIsReadonly ? target : target[RAW];
const proxy = reactiveMap.get(self);
if (proxy && proxy[key] !== void 0) {
return proxy[key];
}
const getter = Reflect.get(target, key, receiver);
// 2. 判断`key`是否是集合类型的方法(比如`add`、`delete`)。
if (
isSymbol(key) ||
key === 'size' ||
OWN_KEYS.has(key)
) {
return getter;
}
if (!isReadonly) {
track(
self,
TrackOpTypes.GET,
key
);
}
// 3. 如果是集合类型的方法,则返回一个经过特殊处理的函数
if (isMap(target) && isMapIterationKey(key)) {
// 返回一个迭代器
return (...args: any[]) => {
// 设置 readonly,禁止修改
pauseTracking();
const result = getter.apply(target, args);
resetTracking();
return {
next() {
return {
done: false,
value: reactive(result.next().value),
};
},
[Symbol.iterator]() {
return this;
},
};
};
}
return isFunction(getter)
? getter.bind(self)
: getter;
};
}
第三幕:mutableCollectionSetter
——集合类型的“Setter卫士”
mutableCollectionSetter
函数的作用是拦截集合类型的set
操作,并在set
操作前后触发依赖收集和触发更新。
// packages/reactivity/src/reactive.ts
function mutableCollectionSetter(target: any, key: string | symbol, value: any, receiver: any) {
// 1. 先执行原始的set操作。
const result = Reflect.set(target, key, value, receiver);
// 2. 触发依赖更新。
trigger(target, TriggerOpTypes.SET, key, value);
return result;
}
第四幕:实战演练——reactive(new Set())
的内部流程
现在,我们来模拟一下reactive(new Set())
的内部流程,看看Vue 3是如何把一个Set
变成响应式的。
-
创建
Set
实例: 首先,我们创建一个Set
的实例。const rawSet = new Set();
-
调用
reactive
函数: 然后,我们调用reactive
函数,把rawSet
变成响应式的。const reactiveSet = reactive(rawSet);
-
创建
Proxy
实例:reactive
函数会创建一个Proxy
实例,用于拦截rawSet
的各种操作。const proxy = new Proxy(rawSet, mutableCollectionHandlers);
-
拦截
add
操作: 当我们调用reactiveSet.add(1)
时,Proxy
会拦截add
操作,并执行mutableCollectionHandlers.get
方法。reactiveSet.add(1); // 触发 Proxy 的 get 拦截
-
createCollectionGetter
发挥作用:mutableCollectionHandlers.get
方法会调用createCollectionGetter
函数,生成一个用于拦截add
操作的getter函数。 -
执行
add
操作:createCollectionGetter
返回的getter函数会先触发依赖收集,然后执行原始的add
操作,最后触发依赖更新。// 触发依赖收集 track(rawSet, TrackOpTypes.GET, 'add'); // 执行原始的add操作 rawSet.add(1); // 触发依赖更新 trigger(rawSet, TriggerOpTypes.ADD, 1);
-
视图更新: 依赖更新会通知所有依赖于
reactiveSet
的视图或者计算属性进行更新,从而保证视图的响应式。
第五幕:readonly
和shallowReactive
的集合类型处理
除了reactive
,Vue 3还提供了readonly
和shallowReactive
两个函数,用于创建只读的和浅层响应式的对象。
对于集合类型,readonly
和shallowReactive
的处理方式略有不同。
-
readonly
:readonly
会创建一个只读的集合类型,任何尝试修改集合类型的操作都会抛出一个错误。 -
shallowReactive
:shallowReactive
只会对集合类型的第一层属性进行响应式处理,如果集合类型的元素是对象,那么这些对象不会被递归地转换为响应式对象。
集合类型的readonly
处理
// packages/reactivity/src/reactive.ts
export const readonlyCollectionHandlers: ProxyHandler<any> = {
get: /*#__PURE__*/ createCollectionGetter(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
},
add(target: any, key: string | symbol): boolean {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
);
}
return true
},
delete(target: any, key: string | symbol): boolean {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
);
}
return true
},
clear(target: any, key: string | symbol): boolean {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
);
}
return true
},
};
总结
特性 | reactive |
readonly |
shallowReactive |
---|---|---|---|
响应式深度 | 深层响应式,集合内部的元素如果是对象也会被递归转换为响应式对象 | 深层只读,集合内部的元素如果是对象也会被递归转换为只读对象 | 浅层响应式,集合内部的元素如果是对象则不会被转换为响应式对象 |
修改集合 | 允许修改集合,修改会触发视图更新 | 禁止修改集合,尝试修改会抛出警告(开发环境) | 允许修改集合,修改会触发视图更新(仅限第一层属性) |
应用场景 | 需要深度响应式且允许修改的场景,例如需要响应式更新的购物车数据、需要响应式更新的用户信息列表等 | 需要只读数据的场景,例如只读的配置信息、只读的用户权限列表等 | 只需要浅层响应式且允许修改的场景,例如只需要响应式更新的表单数据,但表单内部的对象不需要响应式更新等 |
性能考虑 | 性能开销较大,因为需要递归地转换对象为响应式对象 | 性能开销较大,因为需要递归地转换对象为只读对象,并且需要进行额外的只读检查 | 性能开销较小,因为只需要转换第一层属性为响应式对象,无需递归转换 |
适用集合类型 | Set 、Map 、WeakSet 、WeakMap |
Set 、Map 、WeakSet 、WeakMap |
Set 、Map 、WeakSet 、WeakMap |
最后,来个小彩蛋
Vue 3 源码里还对 WeakSet
和 WeakMap
做了特殊处理,因为它们是弱引用集合,垃圾回收机制可能会回收掉集合里的元素,导致响应式失效。所以 Vue 3 对 WeakSet
和 WeakMap
的响应式处理更加谨慎,这里就不展开讲了,有兴趣的同学可以自己去研究源码。
好了,今天的分享就到这里,希望各位靓仔靓女有所收获!下次再见!