Vue3 响应式系统源码解析:Map/Set Collection 响应式核心实现

本文解析 Vue3 reactivity 模块中 Collection(Map/Set/WeakMap/WeakSet) 的响应式处理逻辑,重点在设计思路与源码结构,而不是 API 使用。


1. 背景:为什么 Map/Set 要单独写代码?

Vue 对普通对象 {} 的响应式靠 get/set 即可,但对 Map/Set:

  • 方法是函数 (如 .set().get()
  • 键可能是对象
  • 有迭代器keysvaluesentriesfor...of
  • 弱集合 WeakMap/WeakSet 无法遍历

因此,Vue 必须提供一套专门的"仪表方法"(instrumentations)来接管所有操作。

这一段源码就是专门为此而设计的。


2. createIterableMethod ------ 迭代器逻辑封装

代码片段

javascript 复制代码
function createIterableMethod(method, isReadonly, isShallow) {
  return function (...args) {
    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 = isShallow ? toShallow : isReadonly ? toReadonly : toReactive

    !isReadonly && 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
      },
    }
  }
}

逐段解释:

① 获取原始对象
ini 复制代码
const target = this[ReactiveFlags.RAW]
const rawTarget = toRaw(target)

Map 可能是嵌套 reactive 包装,这里确保拿到真正的原对象。

② 判断返回值是 key/value/key+value
ini 复制代码
const isPair = method === 'entries' ...
const isKeyOnly = method === 'keys'
③ 执行原生迭代器
ini 复制代码
const innerIterator = target[method](...args)
④ 依赖收集
scss 复制代码
track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)

关键点:只要调用 keys、values、entries、for-of,都要追踪依赖。

这样当集合结构变化时才能触发更新。

⑤ 迭代器包装:将每个值转成 reactive
css 复制代码
value: wrap(...)

Map 中存的值可能是对象,Vue 自动转换成 reactive 或 readonly。

这一部分是最核心的"响应式迭代器"实现。


3. createReadonlyMethod ------ 用于 readonly 集合

代码片段

typescript 复制代码
function createReadonlyMethod(type) {
  return function (...args) {
    warn(`${capitalize(type)} operation failed: target is readonly.`)
    return type === TriggerOpTypes.DELETE
      ? false
      : type === TriggerOpTypes.CLEAR ? undefined : this
  }
}

思路:

  • 任何修改(set/add/delete/clear)都会提示错误
  • 并返回合理的 fallback 值

举例:

  • delete()false
  • clear()undefined
  • set/add → 返回 this(保持链式调用)

4. createInstrumentations ------ 生成 Map/Set 全量代理方法

它会根据参数 readonlyshallow 返回不同版本的方法表。


(1)get(key)

代码片段

vbnet 复制代码
get(key) {
  const target = this[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)

  if (!readonly) {
    if (hasChanged(key, rawKey)) track(rawTarget, TrackOpTypes.GET, key)
    track(rawTarget, TrackOpTypes.GET, rawKey)
  }

  const { has } = getProto(rawTarget)

  const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive

  if (has.call(rawTarget, key)) {
    return wrap(target.get(key))
  } else if (has.call(rawTarget, rawKey)) {
    return wrap(target.get(rawKey))
  }
}

关键细节:

① key 和 rawKey 都要 track

原因:用户可能用 reactive(obj) 或 raw obj 作为 map key。

② 始终 wrap 返回值

所有 get 到的 value 必须返回 reactive/readonly 版本。


(2)size 属性

csharp 复制代码
get size() {
  const target = this[ReactiveFlags.RAW]
  !readonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  return target.size
}

size 依赖整个集合结构,所以也要使用 ITERATE 依赖类型。


(3)has(key)

逻辑和 get 类似,也要对 key 与 rawKey 都进行 track。


(4)forEach

Vue 为回调参数做响应式包装:

scss 复制代码
callback.call(thisArg, wrap(value), wrap(key), observed)

确保:

  • value 是 reactive
  • key 是 reactive
  • this 是 reactive

5. Mutation(增删改清)操作

如果不是 readonly,则提供真正的 set/add/delete/clear。


(1)add (for Set)

scss 复制代码
add(value) {
  value = toRaw(value)
  const hadKey = target.has(value)
  if (!hadKey) {
    target.add(value)
    trigger(target, TriggerOpTypes.ADD, value, value)
  }
  return this
}

(2)set (for Map)

核心逻辑

  • 把 value 转 raw
  • 判断 key 是否存在
  • 触发 ADD 或 SET

(3)delete

触发 DELETE 类型依赖,并将旧值传递给 watcher。


(4)clear

触发 CLEAR,且如果是 Map 会克隆 oldTarget(仅 dev)。


6. iterator 方法统一挂载

sql 复制代码
['keys','values','entries',Symbol.iterator].forEach(method => {
  instrumentations[method] = createIterableMethod(method, readonly, shallow)
})

这是响应式迭代器的核心。


7. createInstrumentationGetter ------ Proxy.get 捕获器

它定义了所有 Collection 的代理行为。

vbnet 复制代码
get(target, key, receiver) {
  if (key === ReactiveFlags.IS_REACTIVE) return !isReadonly
  if (key === ReactiveFlags.IS_READONLY) return isReadonly
  if (key === ReactiveFlags.RAW) return target

  return Reflect.get(
    hasOwn(instrumentations, key) && key in target
      ? instrumentations
      : target,
    key,
    receiver,
  )
}

逻辑:

  1. 读标识位(isReactive/isReadonly/raw)
  2. 如果有对应的 instrumentations 方法,则从那里取
  3. 否则取集合原本的方法

8. 最终导出的四种 handler

arduino 复制代码
export const mutableCollectionHandlers
export const shallowCollectionHandlers
export const readonlyCollectionHandlers
export const shallowReadonlyCollectionHandlers

Vue 会根据:

  • reactive()
  • shallowReactive()
  • readonly()
  • shallowReadonly()

来使用不同的 handler。


总结

这段源码是 Vue3 响应式系统中最复杂的一部分之一,核心设计理念包括:

  • 以 instrumentations 表包装所有 Collection 方法
  • 迭代器统一代理,确保 for-of / keys / values 都能被追踪
  • 返回值自动 wrap 成 reactive 或 readonly
  • 同时支持 raw key 与 reactive key 的一致性检查
  • 对不同模式(shallow / readonly)进行统一抽象

它是 Vue 能优雅支持 Map/Set 响应式的关键。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

相关推荐
EndingCoder19 分钟前
类的继承和多态
linux·运维·前端·javascript·ubuntu·typescript
用户479492835691520 分钟前
React 终于出手了:彻底终结 useEffect 的"闭包陷阱"
前端·javascript·react.js
程序员猫哥26 分钟前
前端开发,一句话生成网站
前端
Younglina1 小时前
一个纯前端的网站集合管理工具
前端·vue.js·chrome
木头程序员1 小时前
前端(包含HTML/JavaScript/DOM/BOM/jQuery)基础-暴力复习篇
开发语言·前端·javascript·ecmascript·es6·jquery·html5
卖火箭的小男孩1 小时前
# Flutter Provider 状态管理完全指南
前端
小雨青年1 小时前
鸿蒙 HarmonyOS 6|ArkUI(01):从框架认知到项目骨架
前端
Null1551 小时前
浏览器唤起本地桌面应用(基础版)
前端·浏览器
pas1361 小时前
31-mini-vue 更新element的children
前端·javascript·vue.js
wordbaby1 小时前
TanStack Router 实战:如何构建经典的“左侧菜单 + 右侧内容”后台布局
前端·react.js