分析 Vue 3 源码中对 `Map`、`Set` 等集合类型数据的响应性处理,特别是 `collectionHandlers` 如何拦截 `add`、`delete`、`clear` 等操作。

Vue 3 响应式集合类型:一场“监听风云”的实况转播

各位观众,各位朋友,晚上好!欢迎来到“Vue源码一日游”特别节目。我是今天的导游,名叫……呃,就叫我“源码君”吧!

今天,我们要深入Vue 3的响应式宝库,扒一扒那些“不太安分”的集合类型:MapSet。 它们可不是简单的容器,在Vue 3的魔法加持下,它们的一举一动都会被密切关注,任何风吹草动都会触发视图的更新。

准备好了吗?让我们开始这场“监听风云”的实况转播!

前戏:响应式的基本原理回顾

在深入集合类型之前,我们先快速回顾一下Vue 3响应式的核心机制。Vue 3 使用 Proxy 对数据进行拦截,通过 track 收集依赖,通过 trigger 触发更新。

简单来说:

  1. Proxy 上岗: Vue 3 会用 Proxy 包装你的数据对象,创建一个代理对象。
  2. Track 侦查: 当你在模板中使用某个响应式数据时,Vue 3 会通过 track 函数,把当前组件的 effect 函数(也就是用于更新视图的函数)和这个数据关联起来,建立“依赖关系”。
  3. Trigger 告警: 当你修改响应式数据时,Vue 3 会通过 trigger 函数,找到所有依赖于这个数据的 effect 函数,并执行它们,从而更新视图。

这就像一个警察局(Proxy),警察(track)记录谁经常出入哪个地方(访问哪个响应式数据),一旦这个地方发生了什么事(数据被修改),警察就会通知那些经常出入的人(执行effect函数),让他们采取行动(更新视图)。

正戏:集合类型,你们的“特殊待遇”来了!

普通的属性访问,Proxy 拦截 getset 就够了。但是 MapSet 这些集合类型,它们的操作可不仅仅是简单的赋值和取值,还有 adddeleteclear 等等。为了应对这些“花样百出”的操作,Vue 3 特别定制了一套方案,也就是我们今天要重点讲解的 collectionHandlers

collectionHandlers 本质上是一个对象,它定义了针对不同集合类型(MapSetWeakMapWeakSet)的各种操作的拦截器。 让我们来看一段关键的源码(简化版):

// packages/reactivity/src/collectionHandlers.ts

import { mutableInstrumentations } from './mutableCollection'
import { trackOpBit } from './operations'
import { TrackOpTypes, TriggerOpTypes } from './constants'
import {
  isReadonly,
  isShallow,
  readonly,
  shallowReadonly
} from './reactive'
import {
  iteratorKey,
  ITERATE_KEY,
  MAP_KEY_ITERATE_KEY,
  ReactiveEffect
} from './reactiveEffect'
import { triggerRef } from './ref'
import {
  hasOwn,
  isArray,
  isIntegerKey,
  isMap,
  isSymbol
} from '@vue/shared'
import { track, trigger } from './effect'

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: any, key: string | symbol, receiver: object) {
    const isReadonly = false;
    const isShallow = false;
    // ... 省略部分代码

    // collection type (Map, Set, ...)
    if (key === ReactiveFlags.RAW && receiver === reactiveMap.get(target)) {
      return target
    }
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.IS_SHALLOW
    ) {
      return isShallow
    } else if (
      key === ReactiveFlags.IS_PROXY
    ) {
      return true
    }

    const targetIsArray = isArray(target)

    // handle array access
    if (targetIsArray && isIntegerKey(key)) {
      // make sure to track the length as well
      track(target, TrackOpTypes.GET, key)
      return Reflect.get(target, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    if (isSymbol(key) ? builtInSymbols.has(key) : false) {
      return res
    }

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }
    if (isObject(res)) {
      // Convert returned value into a reactive data structure.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

function createSetter(shallow = false) {
  return function set(
    target: any,
    key: string | symbol,
    value: any,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    }

    const hadKey = isArray(target) && isIntegerKey(key)
      ? Number(key) < target.length
      : hasOwn(target, key)

    const result = Reflect.set(target, key, value, receiver)

    // don't trigger if target is something up in the prototype chain of the `receiver`
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
export const mutableCollectionHandlers: ProxyHandler<any> = {
  get: /*#__PURE__*/ createGetter(),
  set: /*#__PURE__*/ createSetter(),
  has: (target: object, key: string | symbol): boolean => {
    const result = Reflect.has(target, key)
    track(target, TrackOpTypes.HAS, key)
    return result
  },
  ownKeys: (target: object): (string | symbol)[] => {
    track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
    return Reflect.ownKeys(target)
  }
}
export const readonlyCollectionHandlers: ProxyHandler<any> = {
  get: /*#__PURE__*/ createGetter(true),
  set(target: any, key: string | symbol): boolean {
    if (__DEV__) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target: object, key: string | symbol): boolean {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  has: (target: object, key: string | symbol): boolean => {
    const result = Reflect.has(target, key)
    track(target, TrackOpTypes.HAS, key)
    return result
  },
  ownKeys: (target: object): (string | symbol)[] => {
    track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
    return Reflect.ownKeys(target)
  }
}

function getReadonly(isReadonly = false, shallow = false) {
  return function get(target: any, key: string | symbol, receiver: object) {
    // ... 省略部分代码

    // collection type (Map, Set, ...)
    if (key === ReactiveFlags.RAW && receiver === readonlyMap.get(target)) {
      return target
    }
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.IS_SHALLOW
    ) {
      return isShallow
    } else if (
      key === ReactiveFlags.IS_PROXY
    ) {
      return true
    }

    const targetIsArray = isArray(target)

    // handle array access
    if (targetIsArray && isIntegerKey(key)) {
      // make sure to track the length as well
      track(target, TrackOpTypes.GET, key)
      return Reflect.get(target, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    if (isSymbol(key) ? builtInSymbols.has(key) : false) {
      return res
    }

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }
    if (isObject(res)) {
      // Convert returned value into a reactive data structure.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

const shallowReadonlyGet = /*#__PURE__*/ getReadonly(true, true);

export const shallowReadonlyCollectionHandlers: ProxyHandler<any> = {
  get: shallowReadonlyGet,
  set(target: any, key: string | symbol): boolean {
    if (__DEV__) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target: object, key: string | symbol): boolean {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  has: (target: object, key: string | symbol): boolean => {
    const result = Reflect.has(target, key)
    track(target, TrackOpTypes.HAS, key)
    return result
  },
  ownKeys: (target: object): (string | symbol)[] => {
    track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
    return Reflect.ownKeys(target)
  }
}

export const readonlyMap = new WeakMap<object, any>()

export const mutableMap: WeakMap<object, any> = new WeakMap<object, any>()

(源码位置:packages/reactivity/src/collectionHandlers.ts

mutableCollectionHandlersProxy 的一个配置项,包含了 getsethasownKeys方法。

重点来了! collectionHandlers 真正发挥威力的地方,在于它与 mutableInstrumentations 的配合。mutableInstrumentations 是一个对象,里面存储了对 MapSet 等集合类型原生方法的“增强版”。

// packages/reactivity/src/mutableCollection.ts
import { isReadonly, isShallow, reactive, readonly } from './reactive'
import { track, trigger } from './effect'
import {
  TrackOpTypes,
  TriggerOpTypes
} from './constants'
import {
  hasOwn,
  hasChanged,
  isMap,
  isSet
} from '@vue/shared'

const canInstrument = (val: object | undefined) => !!val && '__v_isReactive' in val
export const mutableInstrumentations: Record<string, Function> = {
  get(this: Map<any, any>, key: any) {
    const target = this[RAW]
    const hadKey = has(target, key)
    track(
      target,
      TrackOpTypes.GET,
      key
    )
    if (hadKey) {
      return isReadonly(target)
        ? readonly(target.get(key))
        : isShallow(target)
          ? target.get(key)
          : reactive(target.get(key))
    }
  },
  has(this: Map<any, any> | Set<any>, key: any) {
    const target = this[RAW]
    track(target, TrackOpTypes.HAS, key)
    return target.has(key)
  },
  add(this: Set<any>, value: any) {
    const target = this[RAW]
    const hadKey = target.has(value)
    const result = target.add(value)
    if (!hadKey) {
      trigger(target, TriggerOpTypes.ADD, value)
    }
    return result
  },
  set(this: Map<any, any>, key: any, value: any) {
    const target = this[RAW]
    const hadKey = target.has(key)
    const oldValue = target.get(key)
    const rawValue = value //toRaw(value)
    target.set(key, rawValue)
    if (!hadKey) {
      trigger(target, TriggerOpTypes.ADD, key)
    } else if (hasChanged(rawValue, oldValue)) {
      trigger(target, TriggerOpTypes.SET, key, value)
    }
    return this
  },
  delete(this: Map<any, any> | Set<any>, key: any) {
    const target = this[RAW]
    const hadKey = target.has(key)
    const result = target.delete(key)
    if (hadKey) {
      trigger(target, TriggerOpTypes.DELETE, key)
    }
    return result
  },
  clear(this: Map<any, any> | Set<any>) {
    const target = this[RAW]
    const hadItems = target.size !== 0
    const result = target.clear()
    if (hadItems) {
      trigger(
        target,
        TriggerOpTypes.CLEAR,
        undefined
      )
    }
    return result
  },
  forEach(this: Map<any, any>, callback: Function, thisArg?: any) {
    const target = this[RAW]
    const wrap = (val: any) => isReadonly(target)
      ? readonly(val)
      : isShallow(target)
        ? val
        : reactive(val)
    target.forEach((value: any, key: any) => {
      // important: make sure isReadonly(value) and isShallow(value) is
      // checked for the original value.
      callback.call(thisArg, wrap(value), wrap(key), this)
    })
  },
  keys(this: Map<any, any>, iterationType: ITERATION_KEY = ITERATE_KEY) {
    const target = this[RAW]
    const isReadonly = isReadonly(target)
    const isShallow = isShallow(target)
    const iterator = target.keys()
    return {
      next() {
        const { value, done } = iterator.next()
        if (done) {
          return { value, done }
        }
        return {
          value: wrap(value),
          done: false
        }
      },
      [Symbol.iterator]() {
        return this
      }
    }
  },
  values(this: Map<any, any>, iterationType: ITERATION_KEY = ITERATE_KEY) {
    const target = this[RAW]
    const isReadonly = isReadonly(target)
    const isShallow = isShallow(target)
    const iterator = target.values()
    return {
      next() {
        const { value, done } = iterator.next()
        if (done) {
          return { value, done }
        }
        return {
          value: wrap(value),
          done: false
        }
      },
      [Symbol.iterator]() {
        return this
      }
    }
  },
  entries(this: Map<any, any>, iterationType: ITERATION_KEY = ITERATE_KEY) {
    const target = this[RAW]
    const isReadonly = isReadonly(target)
    const isShallow = isShallow(target)
    const iterator = target.entries()
    return {
      next() {
        const { value, done } = iterator.next()
        if (done) {
          return { value, done }
        }
        return {
          value: [wrap(value[0]), wrap(value[1])],
          done: false
        }
      },
      [Symbol.iterator]() {
        return this
      }
    }
  },
  [Symbol.iterator](this: Map<any, any>, iterationType: ITERATION_KEY = ITERATE_KEY) {
    return this.entries()
  }
}

function wrap(value: any) {
  return isReadonly(value)
    ? readonly(value)
    : isShallow(value)
      ? value
      : reactive(value)
}
; (['keys', 'values', 'entries', Symbol.iterator] as const).forEach(method => {
  mutableInstrumentations[method] = function (this: Map<any, any>, iterationType: ITERATION_KEY = ITERATE_KEY) {
    const target = this[RAW]
    track(target, TrackOpTypes.ITERATE, iterationType)
    // instead of returning the native iterator directly, return a wrapped one.
    // this makes sure all entries/keys/values returned from the iterator are
    // reactive
    return mutableInstrumentations[method].call(target, iterationType)
  }
})

; (['add', 'delete', 'clear', 'set'] as const).forEach(method => {
  mutableInstrumentations[method] = function (this: Map<any, any> | Set<any>, ...args: any[]) {
    const target = this[RAW]
    if (__DEV__) {
      const proto = Object.getPrototypeOf(this)
      if (proto.hasOwnProperty('__v_isReadonly')) {
        console.warn(`Avoid mutating a readonly object value as it results in unexpected behavior.
Object: ${proto}`)
      }
    }
    const result = target[method](...args)
    return result
  }
})

export {
  canInstrument
}

(源码位置:packages/reactivity/src/mutableCollection.ts

现在,当我们使用 reactive 函数将一个 MapSet 变成响应式对象时,Vue 3 会偷偷地把这个响应式对象的原型链上的对应方法替换成 mutableInstrumentations 里的“增强版”方法。

举个例子:

假设我们有以下代码:

import { reactive } from 'vue'

const myMap = reactive(new Map());

myMap.set('name', '源码君'); // 调用增强版的 set 方法

当我们调用 myMap.set('name', '源码君') 时,实际上调用的是 mutableInstrumentations.set 方法。 这个“增强版”的 set 方法做了什么呢?

  1. 获取原始对象: 首先,它会通过 this[RAW] 获取到原始的 Map 对象(没有被 Proxy 代理的那个)。
  2. 执行原生操作: 然后,它会调用原始 Map 对象的 set 方法,真正地把键值对设置进去。
  3. 触发更新: 最关键的一步来了! 在完成原生操作后,它会调用 trigger 函数,通知所有依赖于这个 Map 的组件进行更新。

adddeleteclear 等方法也是类似的流程,都是先执行原生操作,然后触发更新。

精彩瞬间回放:collectionHandlers + mutableInstrumentations

为了让大家更直观地理解,我们用一个表格来总结一下 collectionHandlersmutableInstrumentations 是如何协同工作的:

操作类型 collectionHandlers 职责 mutableInstrumentations 职责
get 拦截 get 操作,但实际的取值操作会被代理到 mutableInstrumentations.get 1. 通过 this[RAW] 获取原始 Map 对象。 2. 调用原始 Map 对象的 get 方法。 3. 使用 track 函数收集依赖。 4. 如果值是对象,则递归地将其转换为响应式对象。
set 拦截 set 操作,但实际的设置操作会被代理到 mutableInstrumentations.set 1. 通过 this[RAW] 获取原始 Map 对象。 2. 调用原始 Map 对象的 set 方法。 3. 使用 trigger 函数触发更新。
add 拦截 add 操作,但实际的添加操作会被代理到 mutableInstrumentations.add 1. 通过 this[RAW] 获取原始 Set 对象。 2. 调用原始 Set 对象的 add 方法。 3. 使用 trigger 函数触发更新。
delete 拦截 delete 操作,但实际的删除操作会被代理到 mutableInstrumentations.delete 1. 通过 this[RAW] 获取原始 MapSet 对象。 2. 调用原始 MapSet 对象的 delete 方法。 3. 使用 trigger 函数触发更新。
clear 拦截 clear 操作,但实际的清空操作会被代理到 mutableInstrumentations.clear 1. 通过 this[RAW] 获取原始 MapSet 对象。 2. 调用原始 MapSet 对象的 clear 方法。 3. 使用 trigger 函数触发更新。
has 拦截 has 操作,但实际的检查操作会被代理到 mutableInstrumentations.has,同时 track 收集依赖 1. 通过 this[RAW] 获取原始 MapSet 对象。 2. 调用原始 MapSet 对象的 has 方法。
ownKeys 拦截 ownKeys 操作, 同时 track 收集依赖 无特殊处理,直接 Reflect.ownKeys(target)

幕后花絮:为什么要这么复杂?

你可能会问:为什么要搞得这么复杂?直接在 Proxyset 拦截器里处理 MapSet 的操作不行吗?

答案是:不行!

因为 MapSet 的操作,例如 adddeleteclear,都不是通过简单的属性赋值来实现的。 如果只拦截 set 操作,就无法监听到这些操作,也就无法实现响应式更新。

Vue 3 的这种方案,通过 collectionHandlersmutableInstrumentations 的配合,实现了对 MapSet 所有操作的全面监听,确保了响应式的完整性。

附加彩蛋:WeakMapWeakSet 的响应式处理

WeakMapWeakSetMapSet 略有不同。 它们是弱引用集合,这意味着它们不会阻止垃圾回收器回收键或值。

由于 WeakMapWeakSet 的键是弱引用的,因此无法像 MapSet 那样进行迭代。 这也意味着无法监听 WeakMapWeakSetsize 属性的变化。

因此,Vue 3 对 WeakMapWeakSet 的响应式处理相对简单,只拦截 getsethasdelete 等操作,而没有对 addclear 等操作进行特殊处理。 此外,由于 WeakMapWeakSet 的键是对象,因此对值的响应式处理与普通对象类似。

总结陈词:监听风云,圆满落幕

今天,我们一起深入 Vue 3 源码,见证了 MapSet 等集合类型在响应式世界里的“特殊待遇”。 通过 collectionHandlersmutableInstrumentations 的巧妙配合,Vue 3 实现了对这些集合类型所有操作的全面监听,确保了响应式的完整性和准确性。

这场“监听风云”的实况转播到此结束。 感谢大家的收看,我们下期再见!

(源码君鞠躬退场)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注