Skip to content

Vue3 对数组、Map、Set 的响应式特殊处理机制,这是 reactivity 系统中最复杂、最有价值的部分,也只有深度开发者才会遇到的知识点。

🧠 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. 数组的响应式特殊处理

Vue3 对数组的关键点是:

2.1 数组下标 + length 的分离依赖追踪

例如:

js
state.arr.length // 依赖 length
state.arr[1]     // 依赖 index=1

当执行:

js
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),阻止这种情况:

js
const arr = reactive([])
effect(() => {
  arr.push(1)   // 会触发 set → trigger → effect 循环
})

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

🧠 4. Map 和 Set 的关键数据结构(超重要)

Vue 的依赖收集核心多了两种类型:

ITERATE_KEY           // for...of、keys()、values() 的依赖
MAP_KEY_ITERATE_KEY   // 特别为 Map 的 key 迭代实现

举例:

js
effect(() => {
  for (const key of map.keys()) {
    console.log(key)
  }
})

Vue 会 track:

track(map, ITERATE_KEY)

这样: 当 map 添加新 key 或删除 key → trigger ITERATE_KEY → 重新执行迭代 effect。

🚀 5. Map.set 的响应式逻辑(非常关键)

源码逻辑(简化版):

ts
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)
  }
}

意义:

  • 新 key:触发“add” → affects ITERATE_KEY(迭代依赖)
  • 旧 key 覆盖:触发“set” → affects key 的依赖

举例:

情况 1:覆盖 key

js
map.set('a', 1)
effect(() => {
  console.log(map.get('a'))
})

map.set('a', 2)   // 触发 get('a') 的 effect

情况 2:新增 key 触发迭代更新

js
effect(() => {
  for (const [k, v] of map) { }
})

map.set('b', 123)
// 会触发迭代 effect,而不是 get('a')

🔥 6. Map.delete 和 clear 的特殊逻辑

ts
delete(key) {
  const hadKey = target.has(key)
  const result = target.delete(key)

  if (hadKey) {
    trigger(target, 'delete', key)
  }
}

delete 会触发:

  • 指向 key 的依赖
  • 迭代依赖(因为 key 减少)

clear() 更是触发所有依赖:

ts
trigger(target, 'clear')

🧩 7. Set.add 的特殊逻辑

ts
add(value) {
  const had = target.has(value)
  target.add(value)

  if (!had) {
    trigger(target, 'add', value)
  }
}

注意:

Set.add 如果重复添加不会触发更新(因为没有变化),Vue 内部会判断这一点。

🌈 8. 迭代器方法(for…of、keys、values、entries)特殊处理

Vue3 对每个迭代器方法都做了代理,比如:

ts
for (const item of reactiveMap) { }

for (const [key, val] of reactiveMap.entries()) { }

reactiveMap.forEach((val, key) => { })

这些都不属于普通 get/set,需要 track:

Map 的迭代依赖:

ts
track(target, ITERATE_KEY)

keys() 额外使用:

ts
track(target, MAP_KEY_ITERATE_KEY)

这样能区分:

  • 改变 key → 会影响 keys()
  • 改变 value → 会影响 entries() / values()

这是 Vue3 设计的极精细之处。

🌟 9. 总结:不同操作触发的依赖类型

数据结构操作tracktrigger
Arraygetindex / length
push/poptemporarily disable tracklength + index
Mapgetkey
setkey / ITERATE_KEYkey 或迭代依赖
deletekey + ITERATE_KEY
for…ofITERATE_KEYadd/delete
keys()MAP_KEY_ITERATE_KEY
SetaddITERATE_KEYadd
deleteITERATE_KEYdelete
for…ofITERATE_KEYadd/delete

🧨 10. 为什么 Vue3 能精确判断“需要更新哪部分”?

靠两层机制:

① track 分不同类型的依赖

  • key 依赖
  • length 依赖
  • 迭代依赖
  • keys 迭代依赖

② trigger 精确调度对应 effect

例如:

js
map.set(key, newVal)

只触发:

  • dep of key
  • iterator dep(如果是新增)

不会乱触发不相关的 effect。

这是 Vue3 性能远超 Vue2 的关键。