Vue3数组、Map、Set 特殊处理
下面整理一份Vue3 对数组、Map、Set 的响应式特殊处理机制,这是 reactivity 系统中最复杂、最有价值的部分,也只有深度开发者才会遇到的知识点。
我会从整体设计 → 为什么需要特殊处理 → Proxy 拦截细节 → track/trigger 不同类型操作 → 源码级实现逻辑解析。
# 🧠 1. 为什么数组、Map、Set 必须额外特殊处理?
因为这 3 类数据结构:
- 有非普通属性访问(length、size)
- 有方法操作(push、delete、set…)
- 有迭代器依赖(for...of、keys、entries)
- 有隐式读取自身(访问 length 会触发 track)
- 有复杂的 key → value 关系(Map/Set 不像普通对象是可枚举 key)
这些都不是简单的“get/set”能处理的。
Vue3 为此专门写了 handlers:
mutableHandlers // 对象
mutableCollectionHandlers // Map/Set 专用
arrayInstrumentations // 数组方法适配
2
3
👇我们逐一讲。
# 🧩 2. 数组的响应式特殊处理
Vue3 对数组的关键点是:
# 2.1 数组下标 + length 的分离依赖追踪
例如:
state.arr.length // 依赖 length
state.arr[1] // 依赖 index=1
2
当执行:
state.arr.push(123)
内部会影响:
- length
- 新的 index(比如 arr[3])
Vue3 会:
# push/pop/shift/unshift/splice 会触发:
trigger(target, 'length')trigger(target, index)
因为一旦变更 length,任何依赖 array.length 的 effect 都要重新执行。
# 2.2 防止 push 中出现递归触发
Vue3 源码中为数组方法包装了一层(arrayInstrumentations),阻止这种情况:
const arr = reactive([])
effect(() => {
arr.push(1) // 会触发 set → trigger → effect 循环
})
2
3
4
Vue 内部会暂时“关闭”依赖收集,执行这些数组方法时不 track。
# 🧩 3. Map 和 Set 的响应式特殊处理
Vue3 的 Map/Set 响应式处理是 reactivity 中最精华、最复杂的部分!
核心难点:
| 操作 | 特性 |
|---|---|
| Map.set | 非覆盖式(新 key) vs 覆盖式(旧 key) |
| Map.delete | 会改变 size |
| Map.forEach / for...of | 迭代依赖要监听所有元素变化 |
| Map.get | 单独依赖 key |
| Set.add | 新增元素 vs 已存在元素 |
Vue3 为 Map/Set 专门实现了:
mutableCollectionHandlers
get
set
add
delete
clear
iterator methods
2
3
4
5
6
7
# 🧠 4. Map 和 Set 的关键数据结构(超重要)
Vue 的依赖收集核心多了两种类型:
ITERATE_KEY // for...of、keys()、values() 的依赖
MAP_KEY_ITERATE_KEY // 特别为 Map 的 key 迭代实现
2
举例:
effect(() => {
for (const key of map.keys()) {
console.log(key)
}
})
2
3
4
5
Vue 会 track:
track(map, ITERATE_KEY)
这样: 当 map 添加新 key 或删除 key → trigger ITERATE_KEY → 重新执行迭代 effect。
# 🚀 5. Map.set 的响应式逻辑(非常关键)
源码逻辑(简化版):
set(key, value) {
const hadKey = target.has(key)
const oldValue = target.get(key)
target.set(key, value)
if (!hadKey) {
trigger(target, 'add', key)
} else if (oldValue !== value) {
trigger(target, 'set', key)
}
}
2
3
4
5
6
7
8
9
10
11
12
# 意义:
- 新 key:触发“add” → affects ITERATE_KEY(迭代依赖)
- 旧 key 覆盖:触发“set” → affects key 的依赖
举例:
# 情况 1:覆盖 key
map.set('a', 1)
effect(() => {
console.log(map.get('a'))
})
map.set('a', 2) // 触发 get('a') 的 effect
2
3
4
5
6
# 情况 2:新增 key 触发迭代更新
effect(() => {
for (const [k, v] of map) { }
})
map.set('b', 123)
// 会触发迭代 effect,而不是 get('a')
2
3
4
5
6
# 🔥 6. Map.delete 和 clear 的特殊逻辑
delete(key) {
const hadKey = target.has(key)
const result = target.delete(key)
if (hadKey) {
trigger(target, 'delete', key)
}
}
2
3
4
5
6
7
8
delete 会触发:
- 指向 key 的依赖
- 迭代依赖(因为 key 减少)
clear() 更是触发所有依赖:
trigger(target, 'clear')
# 🧩 7. Set.add 的特殊逻辑
add(value) {
const had = target.has(value)
target.add(value)
if (!had) {
trigger(target, 'add', value)
}
}
2
3
4
5
6
7
8
注意:
Set.add 如果重复添加不会触发更新(因为没有变化),Vue 内部会判断这一点。
# 🌈 8. 迭代器方法(for…of、keys、values、entries)特殊处理
Vue3 对每个迭代器方法都做了代理,比如:
for (const item of reactiveMap) { }
for (const [key, val] of reactiveMap.entries()) { }
reactiveMap.forEach((val, key) => { })
2
3
4
5
这些都不属于普通 get/set,需要 track:
# Map 的迭代依赖:
track(target, ITERATE_KEY)
# keys() 额外使用:
track(target, MAP_KEY_ITERATE_KEY)
这样能区分:
- 改变 key → 会影响 keys()
- 改变 value → 会影响 entries() / values()
这是 Vue3 设计的极精细之处。
# 🌟 9. 总结:不同操作触发的依赖类型
| 数据结构 | 操作 | track | trigger |
|---|---|---|---|
| Array | get | index / length | |
| push/pop | temporarily disable track | length + index | |
| Map | get | key | |
| set | key / ITERATE_KEY | key 或迭代依赖 | |
| delete | key + ITERATE_KEY | ||
| for…of | ITERATE_KEY | add/delete | |
| keys() | MAP_KEY_ITERATE_KEY | ||
| Set | add | ITERATE_KEY | add |
| delete | ITERATE_KEY | delete | |
| for…of | ITERATE_KEY | add/delete |
# 🧨 10. 为什么 Vue3 能精确判断“需要更新哪部分”?
靠两层机制:
# ① track 分不同类型的依赖
- key 依赖
- length 依赖
- 迭代依赖
- keys 迭代依赖
# ② trigger 精确调度对应 effect
例如:
map.set(key, newVal)
只触发:
- dep of key
- iterator dep(如果是新增)
不会乱触发不相关的 effect。
这是 Vue3 性能远超 Vue2 的关键。