Glittering's blog Glittering's blog
Home
  • 学习手册

    • 《JavaScript教程》
    • 《ES6 教程》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • 《Vite》
    • 《Vue3》
    • TypeScript
    • CSS
  • 技术文档
  • 算法
  • 工作总结
  • 实用技巧
  • collect
About
  • Classification
  • Label
GitHub (opens new window)

Glitz Ma

前端开发工程师
Home
  • 学习手册

    • 《JavaScript教程》
    • 《ES6 教程》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • 《Vite》
    • 《Vue3》
    • TypeScript
    • CSS
  • 技术文档
  • 算法
  • 工作总结
  • 实用技巧
  • collect
About
  • Classification
  • Label
GitHub (opens new window)
  • vue3学习路径
  • Vue3 Virtual DOM & 渲染机制
  • Vue3 响应式核心
  • Vue3数组、Map、Set 特殊处理
    • 🧠 1. 为什么数组、Map、Set 必须额外特殊处理?
    • 🧩 2. 数组的响应式特殊处理
      • 2.1 数组下标 + length 的分离依赖追踪
      • push/pop/shift/unshift/splice 会触发:
      • 2.2 防止 push 中出现递归触发
    • 🧩 3. Map 和 Set 的响应式特殊处理
    • 🧠 4. Map 和 Set 的关键数据结构(超重要)
    • 🚀 5. Map.set 的响应式逻辑(非常关键)
      • 意义:
      • 情况 1:覆盖 key
      • 情况 2:新增 key 触发迭代更新
    • 🔥 6. Map.delete 和 clear 的特殊逻辑
    • 🧩 7. Set.add 的特殊逻辑
    • 🌈 8. 迭代器方法(for…of、keys、values、entries)特殊处理
      • Map 的迭代依赖:
      • keys() 额外使用:
    • 🌟 9. 总结:不同操作触发的依赖类型
    • 🧨 10. 为什么 Vue3 能精确判断“需要更新哪部分”?
      • ① track 分不同类型的依赖
      • ② trigger 精确调度对应 effect
  • Vue3与vue2响应式处理的不同
  • 写一个Mini-vue
  • Mini-vue template增强版
  • 《Vue3》学习笔记
mamingjuan
2025-02-20
目录

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       // 数组方法适配
1
2
3

👇我们逐一讲。


# 🧩 2. 数组的响应式特殊处理

Vue3 对数组的关键点是:

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

例如:

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

当执行:

state.arr.push(123)
1

内部会影响:

  • 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 循环
})
1
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
1
2
3
4
5
6
7

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

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

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

举例:

effect(() => {
  for (const key of map.keys()) {
    console.log(key)
  }
})
1
2
3
4
5

Vue 会 track:

track(map, ITERATE_KEY)
1

这样: 当 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)
  }
}
1
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
1
2
3
4
5
6

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

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

map.set('b', 123)
// 会触发迭代 effect,而不是 get('a')
1
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)
  }
}
1
2
3
4
5
6
7
8

delete 会触发:

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

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

trigger(target, 'clear')
1

# 🧩 7. Set.add 的特殊逻辑

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

  if (!had) {
    trigger(target, 'add', value)
  }
}
1
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) => { })
1
2
3
4
5

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

# Map 的迭代依赖:

track(target, ITERATE_KEY)
1

# keys() 额外使用:

track(target, MAP_KEY_ITERATE_KEY)
1

这样能区分:

  • 改变 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)
1

只触发:

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

不会乱触发不相关的 effect。

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


上次更新: 2025/12/10, 08:07:53
Vue3 响应式核心
Vue3与vue2响应式处理的不同

← Vue3 响应式核心 Vue3与vue2响应式处理的不同→

Copyright © 2015-2025 Glitz Ma
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式