该系列是本人准备面试的笔记,或许有描述不当的地方,请在评论区指出,感激不尽。
其他篇章:
- Promise.try 和 Promise.withResolvers,你了解多少呢?
- 从 babel 编译看 async/await
- 挑战ChatGPT提供的全网最复杂“事件循环”面试题
- Vue.nextTick 从v3.5.13追溯到v0.7.0
- Vue 是怎么从<HelloWorld />、<component is='HelloWorld'>找到HelloWorld.vue
前言
- Set:值成组,独一存,去重快,操作频。
- WeakSet:弱引用,只存对,自动清,不留迹。
- Map:键值对,遍历易,键不限,存储灵。
- WeakMap:弱键值,隐私守,键对象,防泄漏。
为什么会引入 Set、WeakSet、Map 和 WeakMap?
Set 和 WeakSet
Set 和 WeakSet 的设计初衷是解决 Array 在以下场景中的不足:
- 去重功能:
Array无法直接去重,需要使用额外逻辑或工具函数。而Set天然支持去重。 - 性能问题:当数组元素较多时,
Array的查找、删除操作效率较低(时间复杂度为 O(n)),而Set基于哈希表实现,查找和删除的时间复杂度为 O(1)。
WeakSet 的特点
WeakSet 是一种特殊的集合,仅存储对象引用,且这些引用是弱引用(weakly referenced)。
当对象在其他地方没有引用时,垃圾回收机制会自动清除 WeakSet 中的引用,避免内存泄漏。
Map 和 WeakMap
Map 和 WeakMap 的出现则是为了解决 Object 的以下不足:
- 键的局限性:在
Object中,键只能是字符串或Symbol,而Map支持任何类型作为键,包括对象。 - 性能优化:对于较复杂的操作(如使用非字符串类型的键、频繁的插入和删除),
Map提供了更好的性能和灵活性。 - 键值对的存储意图更明确:相比于
Object,Map更适合用作键值对存储,而非仅仅作为原型链和属性的载体。
WeakMap 的特点
WeakMap 是一种键值对集合,键必须是对象且为弱引用。其主要优势包括:
- 内存管理:当某个键对象没有其他引用时,键值对会自动被垃圾回收机制清理。
- 隐私性:
WeakMap的键值对无法被遍历,因此可以用来存储私有数据。
与 Object 和 Array 的对比
功能对比
| 数据结构 | 是否支持去重 | 是否支持任意键 | 是否支持弱引用 | 是否支持遍历 | 性能表现(查找/插入) |
|---|---|---|---|---|---|
| Object | 否 | 字符串/Symbol | 否 | 是 | O(1)(理论上) |
| Array | 否 | 数字/字符串/Symbol | 否 | 是 | O(n) |
| Set | 是 | - | 否 | 是 | O(1) |
| WeakSet | 是 | - | 是 | 否 | O(1) |
| Map | 否 | 任意 | 否 | 是 | O(1) |
| WeakMap | 否 | 对象或非全局注册的符号 | 是 | 否 | O(1) |
基准测试
本地测试结果
测试结果与每个人的硬件,运行环境(浏览器/Node)相关。 大家也可自行测试,在评论区给出你的测试结果。
添加操作
| 数据结构 | 平均时间 (ms) | 最小时间 (ms) | 最大时间 (ms) | 标准差 (ms) |
|---|---|---|---|---|
| Object | 8.48 | 5.43 | 33.36 | 8.30 |
| Map | 5.29 | 3.69 | 10.74 | 2.41 |
| Array | 3.52 | 0.92 | 9.77 | 2.78 |
| Set | 6.25 | 4.32 | 9.04 | 1.54 |
查找操作
| 数据结构 | 平均时间 (ms) | 最小时间 (ms) | 最大时间 (ms) | 标准差 (ms) |
|---|---|---|---|---|
| Object | 0.10 | 0.04 | 0.44 | 0.12 |
| Map | 0.08 | 0.05 | 0.30 | 0.08 |
| Array | 2760.95 | 2733.92 | 2783.76 | 15.06 |
| Set | 0.11 | 0.06 | 0.43 | 0.11 |
更新操作
| 数据结构 | 平均时间 (ms) | 最小时间 (ms) | 最大时间 (ms) | 标准差 (ms) |
|---|---|---|---|---|
| Object | 0.08 | 0.06 | 0.25 | 0.06 |
| Map | 0.10 | 0.05 | 0.34 | 0.08 |
| Array | 9.57 | 9.25 | 10.68 | 0.40 |
删除操作
| 数据结构 | 平均时间 (ms) | 最小时间 (ms) | 最大时间 (ms) | 标准差 (ms) |
|---|---|---|---|---|
| Object | 0.09 | 0.05 | 0.30 | 0.07 |
| Map | 0.09 | 0.05 | 0.40 | 0.10 |
| Array | 3085.05 | 2842.65 | 3282.08 | 144.71 |
| Set | 0.09 | 0.04 | 0.37 | 0.09 |
结果分析
- 对于频繁查询和修改的场景,
Object和Map是优选。 - 对于需要快速唯一性检查或插入的场景,
Set表现出色。 - 尽量避免在大规模数据中频繁对
Array进行删除或查找操作。
注意
- 对于
Set,它是一个值的集合,只存储唯一值。没有类似Map或Object的键值对,因此无法像map.set(key, value)那样直接更新。如果需要修改一个值,只能先删除原值再插入新值。所以不对Set进行 update 测试。 - 对于
Array,删除操作使用splice是常见的实现方式,但它的时间复杂度较高,特别是在删除数组开头或中间元素时。
可以完全取代 Object 吗?
尽管 Map 的功能比 Object 更强大,但两者并非可以完全互相替代:
- 语义化场景:
Object更适合用作存储数据属性的载体(例如:user.name、user.age)。 - 继承机制:
Object支持原型链继承,而Map则没有这个功能。 - 内置方法支持:
Object拥有丰富的内置方法(如Object.keys、Object.values),而Map实例也支持.keys、.values,但返回的是一个迭代器对象。
结论:Map 和 WeakMap 更适合用作键值对存储,而 Object 在结构化数据建模中仍然不可或缺。
使用场景
-
Set 和 WeakSet:
- 去重数组:
Set是处理重复数据的利器,快速过滤重复值。 - 快速集合操作:判断元素是否存在或处理动态集合。
- 弱引用集合(
WeakSet):用于存储不需要强引用的对象集合,比如跟踪 DOM 元素。
- 去重数组:
-
Map 和 WeakMap:
- 复杂键值对映射:
Map能更方便地将对象作为键,存储复杂对象之间的映射关系。 - 缓存管理:
WeakMap是构建内存敏感缓存的理想选择,例如存储与 DOM 元素关联的数据。 - 私有数据存储:使用
WeakMap模拟类的私有属性。
- 复杂键值对映射:
Vue 如何监听变化?
测试 demo 如下:
<script setup>
import { reactive, watch } from "vue";
// Map 示例
const map = reactive(new Map());
const addToMap = () => {
const key = `item${map.size + 1}`;
map.set(key, `value${map.size + 1}`);
};
const deleteFromMap = () => {
const firstKey = Array.from(map.keys())[0];
map.delete(firstKey);
};
// Set 示例
const set = reactive(new Set());
const addToSet = () => {
set.add(`item${set.size + 1}`);
};
const deleteFromSet = () => {
const firstItem = Array.from(set)[0];
set.delete(firstItem);
};
// 监听 Map 变化
watch(
() => Array.from(map.entries()),
(newValue) => {
console.log("Map 已更新:", newValue);
}
);
watch(
() => map.size,
(newValue) => {
console.log("Map.size 已更新:", newValue);
}
);
// 监听 Set 变化
watch(
() => Array.from(set),
(newValue) => {
console.log("Set 已更新:", newValue);
}
);
watch(
() => set.size,
(newValue) => {
console.log("Set.size 已更新:", newValue);
}
);
</script>
源码逻辑:
function createIterableMethod(
method: string | symbol
) {
return function (
this: IterableCollections,
...args: unknown[]
): Iterable<unknown> & Iterator<unknown> {
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const targetIsMap = isMap(rawTarget)
const isPair =
method === 'entries' || (method === Symbol.iterator && targetIsMap)
const isKeyOnly = method === 'keys' && targetIsMap
const innerIterator = target[method](...args)
const wrap = toReactive
track(
rawTarget,
TrackOpTypes.ITERATE,
isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY,
)
return {
next() {
const { value, done } = innerIterator.next()
return done
? { value, done }
: {
value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
done,
}
},
[Symbol.iterator]() {
return this
},
}
}
}
function createInstrumentations(): Instrumentations {
const instrumentations: Instrumentations = {
get(this: MapTypes, key: unknown) {
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.GET, key)
}
track(rawTarget, TrackOpTypes.GET, rawKey)
const { has } = getProto(rawTarget)
const wrap = toReactive
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
} else if (target !== rawTarget) {
target.get(key)
}
},
get size() {
const target = (this as unknown as IterableCollections)[ReactiveFlags.RAW]
track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
},
has(this: CollectionTypes, key: unknown): boolean {
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.HAS, key)
}
track(rawTarget, TrackOpTypes.HAS, rawKey)
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
},
forEach(this: IterableCollections, callback: Function, thisArg?: unknown) {
const observed = this
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = toReactive
track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
},
}
extend(
instrumentations, {
add(this: SetTypes, value: unknown) {
if (!isShallow(value) && !isReadonly(value)) {
value = toRaw(value)
}
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
if (!hadKey) {
target.add(value)
trigger(target, TriggerOpTypes.ADD, value, value)
}
return this
},
set(this: MapTypes, key: unknown, value: unknown) {
if (!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)
}
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)
}
return this
},
delete(this: CollectionTypes, key: unknown) {
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)
}
const oldValue = get ? get.call(target, key) : undefined
const result = target.delete(key)
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
},
clear(this: IterableCollections) {
const target = toRaw(this)
const hadItems = target.size !== 0
const oldTarget = undefined
const result = target.clear()
if (hadItems) {
trigger(
target,
TriggerOpTypes.CLEAR,
undefined,
undefined,
oldTarget,
)
}
return result
},
}
)
const iteratorMethods = [
'keys',
'values',
'entries',
Symbol.iterator,
] as const
iteratorMethods.forEach(method => {
instrumentations[method] = createIterableMethod(method)
})
return instrumentations
}
1. createIterableMethod 方法
createIterableMethod 是一个工厂函数,用于创建 Set 和 Map 上的迭代方法(如 keys, values, entries, Symbol.iterator)的代理。这个方法的目标是将迭代方法包装成一个响应式的迭代器,并在访问时触发数据追踪 (track)。
-
目标: 使得对
Map和Set数据结构的迭代操作(如.keys(),.values(),.entries(),Symbol.iterator)能够触发响应式的数据追踪。 -
如何做:
createIterableMethod方法根据传入的method参数,分别处理Set和Map上的四种常见迭代方法。- 对于
Map,方法还会判断是否需要处理key-value对(isPair)或者单独的key(isKeyOnly)。 - 对
rawTarget进行track调用来追踪数据访问。 - 返回一个
Iterator对象,并且在next方法中封装响应式数据,确保value是经过toReactive包装的。
2. createInstrumentations 方法
createInstrumentations 方法定义了对 Set 和 Map 数据结构的各种操作的响应式拦截,并通过 instrumentations 对象暴露这些拦截方法。这些方法会在访问或修改 Set 和 Map 时触发响应式追踪。
-
目标: 对
Set和Map数据结构的常见操作(如get,set,add,delete,clear等)进行响应式包装,确保对这些操作的每一次修改都能触发依赖的更新。 -
具体操作:
-
get方法:- 在获取
Map中的值时,会先进行toRaw处理,确保操作的是原始数据。 - 使用
track方法记录GET操作的变化,确保依赖能够被追踪。 - 如果键不存在,它会检查原始数据的
key是否存在,并处理返回的值。
- 在获取
-
sizegetter:- 在访问
size属性时,会追踪ITERATE操作(即大小变化),并返回集合的大小。
- 在访问
-
has方法:- 检查
Map或Set是否包含某个键/值,使用track记录该操作。
- 检查
-
forEach方法:- 对集合的每一项调用回调函数,并将每个项包装成响应式数据。
-
add方法 (针对Set):- 如果值不是浅层的,且没有被只读修饰,则对值进行
toRaw转换。 - 如果值已经存在于集合中,则不会触发添加操作。
- 如果值不存在,触发
ADD操作来更新集合,并发出响应式变更。
- 如果值不是浅层的,且没有被只读修饰,则对值进行
-
set方法 (针对Map):- 检查
Map是否已经包含某个键,若不存在,则触发ADD操作。 - 如果值有变化,触发
SET操作。
- 检查
-
delete方法:- 删除集合中的某项,并触发
DELETE操作,发出数据变更通知。
- 删除集合中的某项,并触发
-
clear方法:- 清空集合时,触发
CLEAR操作,确保清空操作被响应式系统正确记录。
- 清空集合时,触发
-
3. 数据代理和追踪
通过 Proxy 和 track 函数,数据结构上的每一次访问和修改都会触发响应式系统的更新。这些追踪信息使得 Vue 3 的响应式系统能够在数据变化时自动更新依赖的视图或其他相关数据。
-
数据修改时触发的响应式操作:
- 任何对
Map和Set数据结构的操作(例如:添加、删除、修改、获取等),都会通过track和trigger方法被追踪和记录。 track用于记录依赖,trigger用于在数据变化时触发更新。
- 任何对
结语
欢乐的时光到这里就要结束了。喜欢这篇文章的朋友不要忘了点赞收藏评论!