解释 Vue 3 源码中 `collectionHandlers` 如何处理 `Map`、`Set` 等集合类型的 `add`、`delete`、`clear` 等操作的响应性。

各位观众,掌声鼓励一下!今天咱们来聊聊 Vue 3 源码里的一个“神秘组织”—— collectionHandlers。 别看名字高大上,其实它就是个“集合管家”,专门负责照顾 MapSet 这些集合类型的响应性。

开场白:集合类型,响应性的“后花园”

在Vue的世界里,数据驱动视图更新是核心理念。对于普通的对象属性,实现响应性相对简单,无非就是监听 getset 操作。但是,MapSet 这些集合类型,它们的操作可不是简单的属性赋值,而是 adddeleteclear 等方法调用。

如果还是用监听 getset 的老办法,那就抓瞎了。想象一下,你往 Mapadd 了一个元素,Vue 却毫无反应,视图纹丝不动,那还玩啥?collectionHandlers 的存在,就是为了解决这个问题,让集合类型的操作也能触发视图更新。

第一幕:collectionHandlers 的真面目

collectionHandlers 本质上是一个对象,里面定义了一系列“拦截器”,专门拦截 MapSet 等集合类型的方法调用。这些“拦截器”会在方法执行前后偷偷地“搞事情”,触发响应性更新。

// packages/reactivity/src/collectionHandlers.ts

import {
  mutableCollectionInstrumentations,
  readonlyCollectionInstrumentations,
  shallowCollectionInstrumentations
} from './collectionUtils'

import {
  mutableInstrumentations,
  readonlyInstrumentations,
  shallowInstrumentations
} from './baseHandlers'

import {
  isReadonly,
  isShallow,
  targetMap,
  track,
  trigger,
  ITERATE_KEY,
  pauseTracking,
  enableTracking
} from './reactive'

import { hasOwn, isObject } from '@vue/shared'

function createCollectionHandler(instrumentations: Record<string, Function>) {
  return {
    get(target: any, key: string | symbol, receiver: any) {
      // 1. 拦截 has 方法
      if (key === 'size') {
        return Reflect.get(target, key, receiver)
      }
      // 2. 拦截其他方法
      //   - 如果 key 存在于 instrumentations 中,则返回 instrumentations 中定义的方法
      //   - 否则,返回原始方法
      return instrumentations[key] || Reflect.get(target, key, receiver)
    },
    has(target: any, key: any) {
      track(target, 'has')
      return Reflect.has(target, key)
    },
    ownKeys(target: any) {
      track(target, ITERATE_KEY)
      return Reflect.ownKeys(target)
    }
  }
}

export const mutableCollectionHandlers: ProxyHandler<any> = createCollectionHandler(
  mutableCollectionInstrumentations
)

export const readonlyCollectionHandlers: ProxyHandler<any> = createCollectionHandler(
  readonlyCollectionInstrumentations
)

export const shallowReadonlyCollectionHandlers: ProxyHandler<any> = createCollectionHandler(
  shallowCollectionInstrumentations
)

简单来说,collectionHandlers 针对 MapSet 等集合类型,重写了 gethasownKeys 这些方法。当访问集合类型的方法时,会先经过 collectionHandlers 的处理,然后再执行原始方法。

第二幕:mutableCollectionInstrumentations,响应性的“魔法棒”

mutableCollectionInstrumentations 是一个对象,里面定义了 adddeleteclear 等方法的“拦截器”。这些“拦截器”会在方法执行前后调用 tracktrigger 函数,从而实现响应性更新。

// packages/reactivity/src/collectionUtils.ts

import { track, trigger, ITERATE_KEY, pauseTracking, enableTracking } from './reactive'
import { isReadonly, isShallow } from './reactive'
import { hasOwn, isObject } from '@vue/shared'

function createInstrumentations() {
  return {
    get(key: any) {
      const target = this[RAW]
      const isReadonly = this[IS_READONLY]
      if (_isReadonly) {
        track(target, key)
      } else if (isShallow(this)) {
        // shallow reactive collection doesn't track
        return target.get(key)
      } else {
        track(target, key)
      }
      const res = target.get(key)

      if (isObject(res)) {
        return isReadonly ? readonly(res) : reactive(res)
      }

      return res
    },
    has(key: any) {
      const target = this[RAW]
      track(target, key)
      return target.has(key)
    },
    add(value: any) {
      const target = this[RAW]
      const result = target.add(value)
      trigger(target, value)
      return result
    },
    set(key: any, value: any) {
      const target = this[RAW]
      const result = target.set(key, value)
      trigger(target, key)
      return result
    },
    delete(key: any) {
      const target = this[RAW]
      const hadKey = target.has(key)
      const result = target.delete(key)
      if (hadKey) {
        trigger(target, 'delete')
      }
      return result
    },
    clear() {
      const target = this[RAW]
      const hadItems = target.size !== 0
      const result = target.clear()
      if (hadItems) {
        trigger(target, ITERATE_KEY)
      }
      return result
    },
    forEach(callback: Function, thisArg?: any) {
      const target = this[RAW]
      const isReadonly = this[IS_READONLY]
      pauseTracking()
      try {
        target.forEach((value: any, key: any) => {
          // we have to double check because the target may self-mutate during
          // iteration.
          track(target, key)
          value = isReadonly ? readonly(value) : reactive(value)
          callback.call(thisArg, value, key, this)
        })
      } finally {
        enableTracking()
      }
    },
    *keys() {
      const target = this[RAW]
      const iterator = target.keys()
      let current
      return {
        next() {
          track(target, current)
          current = iterator.next()
          return current
        },
        [Symbol.iterator]() {
          return this
        }
      }
    },
    *values() {
      const target = this[RAW]
      const iterator = target.values()
      let current
      return {
        next() {
          track(target, current)
          current = iterator.next()
          return current
        },
        [Symbol.iterator]() {
          return this
        }
      }
    },
    *entries() {
      const target = this[RAW]
      const iterator = target.entries()
      let current
      return {
        next() {
          track(target, current)
          current = iterator.next()
          return current
        },
        [Symbol.iterator]() {
          return this
        }
      }
    },
    *[Symbol.iterator]() {
      return this.entries()
    },
    get size() {
      const target = this[RAW]
      track(target, ITERATE_KEY)
      return Reflect.get(target, 'size', this)
    }
  }
}

export const mutableCollectionInstrumentations: Record<string, Function> =
  createInstrumentations()

export const readonlyCollectionInstrumentations: Record<string, Function> =
  createInstrumentations()

export const shallowCollectionInstrumentations: Record<string, Function> =
  createInstrumentations()

咱们以 add 方法为例,看看它是怎么“搞事情”的:

add(value: any) {
  const target = this[RAW] // 获取原始的 Map/Set 对象
  const result = target.add(value); // 执行原始的 add 操作
  trigger(target, value); // 触发响应性更新
  return result;
}

可以看到,add 方法先执行原始的 add 操作,然后调用 trigger 函数,通知 Vue 有数据发生了变化。Vue 就会重新渲染视图,从而实现响应性更新。

第三幕:tracktrigger,响应性的“发动机”

tracktrigger 是 Vue 3 响应式系统的核心函数。track 函数用于收集依赖,trigger 函数用于触发更新。

  • track(target, key): 当读取响应式数据时,track 函数会被调用,它会将当前正在执行的副作用函数(比如渲染函数)与该数据关联起来,建立依赖关系。

  • trigger(target, key): 当响应式数据发生变化时,trigger 函数会被调用,它会找到所有与该数据关联的副作用函数,并执行它们,从而触发视图更新。

collectionHandlers 中,tracktrigger 函数被巧妙地运用,实现了集合类型的响应性更新。

举个栗子:Map 的响应性

假设我们有一个 Map 对象:

const map = reactive(new Map());

现在,我们往 mapadd 一个元素:

map.add('name', 'Vue');

这个 add 操作会触发 mutableCollectionInstrumentations.add 方法的执行。add 方法会先执行原始的 add 操作,然后调用 trigger(map, 'name')trigger 函数会找到所有依赖于 map 的副作用函数,并执行它们,从而触发视图更新。

表格总结:collectionHandlers 的“拦截器”

方法 拦截器 作用
add mutableCollectionInstrumentations.add 执行原始 add 操作,然后调用 trigger 函数,触发响应性更新。
delete mutableCollectionInstrumentations.delete 执行原始 delete 操作,如果删除成功,则调用 trigger 函数,触发响应性更新。
clear mutableCollectionInstrumentations.clear 执行原始 clear 操作,如果清空成功,则调用 trigger 函数,触发响应性更新。
get mutableCollectionInstrumentations.get 执行原始 get 操作,然后调用 track 函数,收集依赖。
has mutableCollectionInstrumentations.has 执行原始 has 操作,然后调用 track 函数,收集依赖。
set mutableCollectionInstrumentations.set 执行原始 set 操作,然后调用 trigger 函数,触发响应性更新。
forEach mutableCollectionInstrumentations.forEach 执行原始 forEach 操作,并在每次迭代时调用 track 函数,收集依赖。
keys mutableCollectionInstrumentations.keys 执行原始 keys 操作,并在每次迭代时调用 track 函数,收集依赖。
values mutableCollectionInstrumentations.values 执行原始 values 操作,并在每次迭代时调用 track 函数,收集依赖。
entries mutableCollectionInstrumentations.entries 执行原始 entries 操作,并在每次迭代时调用 track 函数,收集依赖。
size mutableCollectionInstrumentations.size 读取原始 size 属性,然后调用 track 函数,收集依赖。

代码示例:一个简单的 Map 响应式组件

<template>
  <div>
    <p>Map Size: {{ map.size }}</p>
    <ul>
      <li v-for="(value, key) in map" :key="key">{{ key }}: {{ value }}</li>
    </ul>
    <button @click="addItem">Add Item</button>
    <button @click="deleteItem">Delete Item</button>
    <button @click="clearMap">Clear Map</button>
  </div>
</template>

<script>
import { reactive } from 'vue';

export default {
  setup() {
    const map = reactive(new Map());
    map.set('name', 'Vue');
    map.set('version', '3');

    const addItem = () => {
      map.set(Date.now(), 'New Item');
    };

    const deleteItem = () => {
      if (map.size > 0) {
        const key = map.keys().next().value;
        map.delete(key);
      }
    };

    const clearMap = () => {
      map.clear();
    };

    return {
      map,
      addItem,
      deleteItem,
      clearMap,
    };
  },
};
</script>

在这个组件中,我们使用 reactive 函数将 Map 对象转换为响应式数据。当 Map 对象发生变化时,组件会自动更新视图。这都要归功于 collectionHandlers 的“幕后工作”。

深入思考:ITERATE_KEY 的作用

collectionHandlers 中,ITERATE_KEY 是一个特殊的 key,用于表示集合类型的迭代。当集合类型发生 adddeleteclear 等操作时,trigger(target, ITERATE_KEY) 会被调用,通知所有依赖于集合类型迭代的副作用函数进行更新。

例如,在上面的组件中,v-for="(value, key) in map" 依赖于 map 的迭代。当 map 发生 adddeleteclear 等操作时,trigger(map, ITERATE_KEY) 会被调用,v-for 会重新渲染,从而更新视图。

PauseTrackingEnableTracking
forEach的实现里,你可能会有疑问,为什么会有pauseTrackingenableTracking

这是因为在 forEach 循环内部,我们可能会读取到 Map 中的值,而读取操作会触发 track 函数,建立依赖关系。但是,我们并不希望在 forEach 循环内部建立额外的依赖关系,因为这可能会导致不必要的更新。

因此,我们在 forEach 循环开始前调用 pauseTracking 函数,暂停依赖收集。在 forEach 循环结束后,我们调用 enableTracking 函数,恢复依赖收集。这样,我们就可以避免在 forEach 循环内部建立额外的依赖关系。

总结:collectionHandlers,响应性的“守护者”

collectionHandlers 是 Vue 3 响应式系统的重要组成部分。它通过拦截 MapSet 等集合类型的方法调用,实现了集合类型的响应性更新。有了 collectionHandlers,我们就可以放心地使用集合类型,而不用担心响应性问题。

希望今天的讲座能够帮助大家更好地理解 Vue 3 源码中的 collectionHandlers。 咱们下期再见!

发表回复

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