Vue3 源码解读之非原始值的响应式原理

Vue.js 3 中的响应式数据是基于 ES6 中的Proxy 实现的,Proxy 可以为其它对象创建一个代理对象。Proxy 除了可以代理 Object、Array、还可以代理 ES6 中新增的 Map、Set、WeakMap、WeakSet 等集合类型。本文将会深入解析Proxy如何对这些数据结构进行代理。

1、基本操作与复合操作

1.1 什么是基本操作

对一个对象进行读取、设置属性值的操作,就属于基本语义的操作,即基本操作,如下代码所示

obj.foo   // 读取属性 foo 的值
obj.foo++ // 读取和设置属性 foo 的值

1.2 什么是复合操作

调用对象下的方法就是典型的非基本操作,即复合操作

obj.fn()

调用一个对象下的方法,是由两个基本语义组成的。第一个语义是 get,即先通过 get 操作得到 obj.fn 属性。第二个基本语义是函数调用,即通过 get 得到 obj.fn 的值后再调用它。

2、代理 Object

2.1 属性读取操作的拦截

一个普通对象的读取操作有以下三种:

  • 访问属性:obj.foo
  • 判断对象或原型上是否存在指定的 key:key in obj
  • 使用 for…in 循环遍历对象:for (const key in obj) {}

对于这些读取操作,Vue.js 的响应系统都会进行拦截,以便当数据变化时能够正确的触发响应。接下来,我们将分别介绍如何拦截这些读取操作。

2.1.1 访问属性的拦截

对于属性的读取,例如 obj.foo,可以通过 Proxy 的 get 拦截函数实现:

const obj = new Proxy({}, {
  // get 拦截函数拦截属性的读取操作
  get: function (target, propKey, receiver) {
    console.log(`getting ${propKey}!`);
    return Reflect.get(target, propKey, receiver);
  },
});

Vue.js 3 中 get 拦截函数的实现如下代码所示:

// packages/reactivity/src/baseHandlers.ts

// get 操作的拦截
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 访问对应标志位的处理逻辑
    if (key === ReactiveFlags.IS_REACTIVE) {
      // 是否是响应式
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      // 是否是只读
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      // 是否是深浅响应
      return shallow
    } else if (
      // receiver指向调用者,这里的判断是为了保证触发拦截handler的是proxy对象本身而非proxy的继承者。
      // 触发拦截器的两种途径:
      // 1 访问proxy对象本身的属性;
      // 2 访问对象原型链上有proxy对象的对象的属性,因为查询属性会沿着原型链向下游依次查询,因此同样会触发拦截器
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      // 返回target本身,即响应式对象的原始值
      return target
    }

    // 访问重写后的 Array 对象的方法,这些方法存储在 arrayInstrumentations 工具集里
    const targetIsArray = isArray(target)

    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      // 这里在调用indexOf等数组方法时是通过proxy来调用的,因此
      // arrayInstrumentations[key]的this一定指向proxy实例,也即receiver
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    // 使用 Reflect.get 返回读取的属性值,第三个参数 receiver 可以帮助分析 this 指向的是谁
    const res = Reflect.get(target, key, receiver)

    // key是symbol或访问的是__proto__属性不做依赖收集和递归响应式转化,直接返回结果
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    // target 为非只读时才需要收集依赖
    // 只读的因为属性不会变化,因此无法触发setter,也就不会触发依赖更新
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    // 如果是浅响应,则直接返回原始值结果
    if (shallow) {
      return res
    }

    // 访问属性已经是ref对象,保证访问ref属性时得到的是ref对象的value属性值,数组、NaN、空字符除外
    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    // 如果原始值结果是一个对象,则继续将其包装成响应式数据
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      // 如果数据为只读,则调用 readonly 对值进行包装
      // 否则调用 reactive 将结果包装成 响应式数据 并返回
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

在 get 拦截函数中,首先判断拦截的 key 是否是 Vue 内部定义的 ReactiveFlags 标志,如果是,则返回 createGetter 在不同场景中被调用时传入 isReadonly 或 shallow 参数的值。

然后是对数组元素或属性 “读取” 操作的拦截。例如通过索引访问数组元素值 ( arr[0] )、访问数组的长度 (arr.length ) 或 使用indexOf、includes 等数组方法查找元素时,都会触发 length 属性以及通过索引访问元素值等读取操作。

// 访问重写后的 Array 对象的方法,这些方法存储在 arrayInstrumentations 工具集里
const targetIsArray = isArray(target)

if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
  // 这里在调用indexOf等数组方法时是通过proxy来调用的,因此
  // arrayInstrumentations[key]的this一定指向proxy实例,也即receiver
  return Reflect.get(arrayInstrumentations, key, receiver)
}

接下来通过 Reflect.get 函数来获取被拦截的 key 的属性值,根据被拦截的 key 的类型以及其属性值的类型来返回不同的结果。

// 使用 Reflect.get 返回读取的属性值,第三个参数 receiver 可以帮助分析 this 指向的是谁
const res = Reflect.get(target, key, receiver)

如果被拦截的 key 是一个 Symbol 或者 key 是原型链上的属性,则不做依赖收集并且直接返回属性的读取结果,不对其做响应式处理。

// key是symbol或访问的是__proto__属性,则不做依赖收集和递归响应式转化,直接返回结果
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
  return res
}

如果被拦截的属性是只读的,那么就没有必要为只读数据建立响应联系。因此,只有在被拦截属性是可读时才调用 track 函数收集依赖,建立响应联系。

// target 为非只读时才需要收集依赖
// 只读的因为属性不会变化,因此无法触发setter,也就不会触发依赖更新
if (!isReadonly) {
  track(target, TrackOpTypes.GET, key)
}

在完成依赖收集之后,需要根据不同的情况来返回不同的读取结果。如果代理的对象只是浅响应,则直接返回读取结果。如果读取的结果是一个 ref 对象,则需要对其进行脱 ref ,返回 res.value 。如果读取结果是一个对象,则继续将其包装成响应式数据。

// 如果是浅响应,则直接返回原始值结果
if (shallow) {
  return res
}

// 访问属性已经是ref对象,保证访问ref属性时得到的是ref对象的value属性值,数组、NaN、空字符除外
if (isRef(res)) {
  // ref unwrapping - does not apply for Array + integer key.
  const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
  return shouldUnwrap ? res.value : res
}

// 如果原始值结果是一个对象,则继续将其包装成响应式数据
if (isObject(res)) {
  // Convert returned value into a proxy as well. we do the isObject check
  // here to avoid invalid value warning. Also need to lazy access readonly
  // and reactive here to avoid circular dependency.
  // 如果数据为只读,则调用 readonly 对值进行包装
  // 否则调用 reactive 将结果包装成 响应式数据 并返回
  return isReadonly ? readonly(res) : reactive(res)
}

2.1.2 in 操作符的拦截

有时候,我们会通过 in 操作符来判断对象或原型上是否存在指定的 key。在 ECMA 规范中,in 操作符的运算结果是通过调用一个叫做 HasProperty 的抽象方法得到的。HasProperty 抽象方法的返回值是通过调用对象的内部方法 [[HasProperty]] 得到的,而 [[HasProperty]] 内部方法对应的拦截函数是 has,因此可以通过 has 拦截函数实现对 in 操作符的代理。

// packages/reactivity/src/baseHandlers.ts

// 通过 has 拦截函数拦截 in 操作符
function has(target: object, key: string | symbol): boolean {
  // 通过 Reflect.has 函数获取结果
  const result = Reflect.has(target, key)
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    // 触发更新
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}

可以看到,对 in 操作符进行拦截时,通过 Reflect.has 函数来获取结果,如果被拦截的 key 不是 Symbol 值,则需要收集依赖,建立响应联系。

2.1.3 for…in 循环的拦截

对于 for…in 的拦截,可以使用 ownKeys 拦截函数来拦截。

// packages/reactivity/src/baseHandlers.ts

// for...in 循环的拦截
function ownKeys(target: object): (string | symbol)[] {
  // 如果操作目标 target 是数组,则使用 length 属性作为 key 并建立响应联系,
  // 否则使用 ITERATE_KEY 建立响应联系
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  // 使用 Reflect.ownKeys(obj) 来获取只属于对象自身拥有的键
  return Reflect.ownKeys(target)
}

在上面的代码中拦截 ownKeys 操作即可间接拦截 for…in 循环。在使用 track 函数进行追踪的时候,将操作类型设置为 TrackOpTypes.ITERATE,同时将 ITERATE_KEY 作为追踪的 key,是因为ownKeys 函数只能拿到目标对象target,不能像 get/set 那样可以得到具体操作的 key。

ownKeys 用来获取一个对象的所有属于自己的键值,这个操作明显不与任何具体的键进行绑定,因此我们只能够构造唯一的 key 作为标识,即 ITERATE_KEY 。

2.2 设置属性操作的拦截

无论是添加新属性,还是修改已有的属性值,其基本的语义都是 [[Set]],因此都是通过 set 函数来拦截。

// packages/reactivity/src/baseHandlers.ts

// set操作符的拦截
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    // 如果旧值为只读,且旧值为 ref,并且新值不是 ref,则不能设置值
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false
    }

    // 如果是浅响应并且 value 为非只读
    if (!shallow && !isReadonly(value)) {
      // value 为浅响应
      if (!isShallow(value)) {
        // 获取原始值
        value = toRaw(value)
        oldValue = toRaw(oldValue)
      }
      // target不是数组,且旧值为ref,新值非ref,直接将ref.value更新为新值
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    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 original
    // 这里判断receiver是proxy实例才更新派发,防止通过原型链触发拦截器触发更新。
    // target === receiver.raw ,说明 receiver 就是 target 的代理对象
    // 只有当 receiver 是target 的代理对象时才触发更新
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 操作类型为 ADD 时,触发响应
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 比较新值与旧值,只有当它们不全等,并且都不是 NaN 的时候才触发响应
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

在 set 拦截函数中,对设置属性操作的拦截可以分为四个部分:

1、值为只读时的拦截

如果旧值为只读,且旧值为 ref 对象,并且新值不是 ref 对象,则不能设置值,返回 false,表示设置值失败。

// 如果旧值为只读,且旧值为 ref,并且新值不是 ref,则不能设置值
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
  return false
}

2、数据为浅响应且值为非只读时的拦截

如果数据为浅响应,并且值为非只读时,如果要设置的值也是浅响应,那么调用 toRaw 方法获取原始值,即通过代理对象的 raw 属性读取原始数据。只有当代理对象 target 和要设置的值value不是 ref 对象时,才能将旧值更新为新值。

// 如果是浅响应并且 value 为非只读
if (!shallow && !isReadonly(value)) {
  // value 为浅响应
  if (!isShallow(value)) {
    // 获取原始值
    value = toRaw(value)
    oldValue = toRaw(oldValue)
  }
  // target不是数组,且旧值为ref,新值非ref,直接将ref.value更新为新值
  if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
} 

3、正常属性的新增和修改的拦截

对于属性正常的新增和拦截,则直接调用 Reflect.set 方法将值赋值给对象的属性,将其设置结果存储到 result 变量中。

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

4、触发响应

根据 ECMA 规范,[[Set]] 内部方法的执行流程中,如果设置的属性不存在于对象上,那么会取得其原型,并调用原型的 [[Set]] 方法,这就会产生由原型引起更新的问题。为了避免这个问题,需要判断 receiver 是否是 target 的代理对象, 即 proxy 实例,只有当 receiver 是target 的代理对象时才触发更新。在源码里调用toRaw方法判断 receiver是否是proxy实例,即通过代理对象的 raw 属性读取原始数据,确定receiver 是否是 target 的代理对象。

然后比较新值与旧值,只有当它们不全等,并且都不是 NaN 的时候才调用 trigger 触发响应。

// 这里判断receiver是proxy实例才更新派发,防止通过原型链触发拦截器触发更新。
// target === receiver.raw ,说明 receiver 就是 target 的代理对象
// 只有当 receiver 是target 的代理对象时才触发更新
if (target === toRaw(receiver)) {
  if (!hadKey) {
    // 操作类型为 ADD 时,触发响应
    trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue)) {
    // 比较新值与旧值,只有当它们不全等,并且都不是 NaN 的时候才触发响应
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
}

2.3 delete 操作符的拦截

在 ECMA 规范中,delete 操作符的行为依赖于 [[Delete]] 内部方法,该内部方法可以使用 deleteProperty 拦截。

// packages/reactivity/src/baseHandlers.ts

// delete 操作符的拦截
function deleteProperty(target: object, key: string | symbol): boolean {
  // 检查被操作的属性是否是对象自己的属性
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  // 使用 Reflect.deleteProperty 完成属性的删除
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    // 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

在 delete 操作符的拦截函数 deleteProperty 中,调用了 Reflect.deleteProperty 来完成属性的删除。如果当前被删除的属性是对象自己的属性并且成功删除时,调用 trigger 触发更新。由于删除操作会使得对象的键变少,它会影响 for…in 循环的次数,因此当操作类型为 DELETE 时,也应该触发那些与 ITERATE_KEY 相关联的副作用函数重新执行。如下面源码中 tirgger 函数中的代码:

// packages/reactivity/src/effect.ts

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  let deps: (Dep | undefined)[] = []
  
  // 省略部分代码
  
  else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
        
      // 省略部分代码
       
      // 操作类型为 DELETE 时,触发与 ITERATE_KEY 相关联的副作用函数重新执行
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          // 取得与 ITERATE_KEY 相关联的副作用函数	
          // 将与 ITERATE_KEY 相关联的副作用函数也添加到 deps
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
     
     // 省略部分代码
    }
  }

  // 省略部分代码
}

3、代理数组

3.1 数组元素或属性的读取拦截

对数组元素或属性的读取操作有以下几种:

  • 通过索引访问数组元素值:arr[0]
  • 访问数组的长度:arr.length
  • 把数组作为对象,使用 for…in 循环遍历
  • 使用 for…of 迭代遍历数组
  • 数组的原型方法,如concat/join/every/some/find/findIndex/includes 等,以及其它所有不改变原数组的原型方法

当这些操作发生时,Vue.js 的响应系统都会进行拦截,以便当数据变化时能够正确的触发响应。

3.1.1 for…in 操作符的拦截

把数组作为对象,使用 for…in 循环遍历时,使用 ownKeys 拦截函数来拦截。如下面的源码所示:

// packages/reactivity/src/baseHandlers.ts

// for...in 循环的拦截
function ownKeys(target: object): (string | symbol)[] {
  // 如果操作目标 target 是数组,则使用 length 属性作为 key并建立响应联系,
  // 否则使用 ITERATE_KEY 建立响应联系
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  // 使用 Reflect.ownKeys(obj) 来获取只属于对象自身拥有的键
  return Reflect.ownKeys(target)
}

可以看到,在 ownKeys 拦截函数内,判断当前操作目标 target 是否是数组,如果是,则使用 length 作为 key 去建立响应联系。

3.1.2 for…of 操作符的拦截

for…of 是用来遍历可迭代对象的,由于数组内建了 Symbol.iterator 方法,因此默认情况下数组可以使用 for…of 遍历。在使用 for…of 遍历时,会读取数组的 Symbol.iterator 属性。该属性是一个 symbol 值,为了避免发生意外的错误,以及性能上的考虑,在 get 拦截函数内不应该在副作用函数与 Symbol.iterator 这类 symbol 值之间建立响应联系。如下面的代码所示:

// packages/reactivity/src/baseHandlers.ts

// get 操作的拦截
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    
    // 省略部分代码
    
    // 使用 Reflect.get 返回读取的属性值,第三个参数 receiver 可以帮助分析 this 指向的是谁
    const res = Reflect.get(target, key, receiver)

    // key是symbol或访问的是__proto__属性不做依赖收集和递归响应式转化,直接返回结果
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    // target 为非只读时才需要收集依赖
    // 只读的因为属性不会变化,因此无法触发setter,也就不会触发依赖更新
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    // 省略部分代码
    
    return res
  }

在上面的源码中,判断 key 的类型是否是 symbol ,如果是,则直接返回读取结果,不在副作用函数与 Symbol.iterator 这类 symbol 值之间建立响应联系,即不调用 track 函数收集依赖。

3.1.3 数组的查找方法的拦截

数组的 includes、indexOf、lastIndexOf 等方法都属于查找方法。这类查找方法在执行的过程中,它会访问数组的 length 属性以及数组的索引,它会通过索引读取数组元素的值。当执行arr.includes 时,可以理解为读取代理对象 arr 的 includes 属性,这会触发 get 拦截函数,因此需要在 get 拦截函数中对这些方法进行拦截,如下面的代码所示:

// packages/reactivity/src/baseHandlers.ts

// get 操作的拦截
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    
    // 省略部分代码

    // 访问重写后的 Array 对象的方法,这些方法存储在 arrayInstrumentations 工具集里
    const targetIsArray = isArray(target)

    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      // 这里在调用indexOf等数组方法时是通过proxy来调用的,因此
      // arrayInstrumentations[key]的this一定指向proxy实例,也即receiver
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    // 省略部分代码
    
  }
}

在上面的源码中,判断代理对象 target 是否是数组,如果是数组并且读取的键值是 includes 、indexOf、lastIndexOf 等,则返回定义在 arrayInstrumentations 对象中相应键值的方法,从而对这些方法进行重写。自定义的 includes/indexOf/lastIndexOf 方法的实现代码如下所示:

// packages/reactivity/src/baseHandlers.ts

const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()

function createArrayInstrumentations() {
  const instrumentations: Record<string, Function> = {}
  // instrument identity-sensitive Array methods to account for possible reactive
  // values
  // 重写 数组的查找方法
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      // toRaw 通过代理对象的 raw 属性读取原始数组对象
      const arr = toRaw(this) as any
      // 遍历数组,按照数组的下标收集依赖
      for (let i = 0, l = this.length; i < l; i++) {
        track(arr, TrackOpTypes.GET, i + '')
      }
      // we run the method using the original args first (which may be reactive)
      // 优先使用原始参数来执行 Array 原型上的方法来查找值
      const res = arr[key](...args)
      // indexOf、lastIndexOf 方法没有找到指定的值,会返回 -1
      // includes 方法没有找到指定的值,会返回 false
      if (res === -1 || res === false) {
        // if that didn't work, run it again using raw values.
        // 没有找到对应的值,args 有可能是包装后的响应式数据,因此获取原始数据后再尝试去查询
        return arr[key](...args.map(toRaw))
      } else {
        return res
      }
    }
  })
  
  // 省略部分代码
  
  return instrumentations
}

可以看到,在重写 includes/indexOf/lastIndexOf 等方法时,加入了依赖收集,使得这些方法具有响应的能力。

3.2.4 隐式修改数组长度的原型方法的拦截

数组的栈方法,例如 push/pop/shift/unshift 以及splice 方法会隐式地修改数组长度。

根据 ECMAScript 规范,这些方法在执行的过程中,既会读取数组的 length 属性值,也会设置数组的 length 属性值。

这些方法在调用时会间接读取/设置 length 属性,循环往复,就会导致调用栈溢出。为了避免这个问题,需要重写这些方法,屏蔽对 length 属性的读取,从而避免在它与副作用函数之间建立响应联系。如下面的代码所示:

const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()

function createArrayInstrumentations() {
  const instrumentations: Record<string, Function> = {}
  
  // 省略部分代码
  
  // instrument length-altering mutation methods to avoid length being tracked
  // which leads to infinite loops in some cases (#2137)
  // 重写 改变数组长度的方法
  // push/pop/shift/unshift 以及splice 方法会隐式地修改数组长度
  // 这些方法在执行的过程中,既会读取数组的 length 属性值,也会设置数组的 length 属性值
  // 因此需要重写这些方法,屏蔽对 length 属性的读取,从而避免在它与副作用函数之间建立响应联系。
  ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
    instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
      // 执行前禁用依赖收集,
      /**
       * 这里主要是为了避免改变数组长度时,会set length,形成track - trigger的死循环
       * 因此要暂停改变数组长度时的执行期间收集依赖
       */
      pauseTracking()
      // push/pop/shift/unshift/splice 方法的默认行为
      const res = (toRaw(this) as any)[key].apply(this, args)
      // 在调用原始方法之后,恢复原来的行为,即允许追踪
      resetTracking()
      return res
    }
  })
  return instrumentations
}

在上面的源码中,在执行 push/pop/shift/unshift/splice 方法的默认行为之前,调用 pauseTracking 函数来停止依赖收集,当这些方法的默认行为执行完毕后,再执行 resetTracking 函数来恢复依赖收集。

pauseTracking 函数和 resetTracking 函数的定义如下:

// packages/reactivity/src/effect.ts

export let shouldTrack = true
const trackStack: boolean[] = []

export function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}

export function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}

可以看到,在上面的代码中,定义了一个标记变量 shouldTrack,它是一个布尔值,代表是否允许执行依赖收集。当 pauseTracking 函数被调用,即在执行 push/pop/shift/unshift/splice 方法的默认行为之前,标记变量 shouldTrack 的值会被设为 false,即停止依赖收集。当 resetTracking 函数被调用,即在执行 push/pop/shift/unshift/splice 方法的默认行为之后,标记变量 shouldTrack 的值会被设为 true,即恢复依赖收集。

在收集依赖的 track 函数中,就会根据标记变量 shouldTrack 的值来决定是否收集依赖。如下面的代码所示:

export function track(target: object, type: TrackOpTypes, key: unknown) {
  
  // 标记变量 shouldTrack,代表是否允许执行依赖收集
  if (shouldTrack && activeEffect) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep()))
    }

    const eventInfo = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined

    trackEffects(dep, eventInfo)
  }
}

可以看到,只有当标记变量 shouldTrack 的值为 true 时,才会执行依赖收集。这样,当 push/pop/shift/unshift/splice 方法间接读取 length 属性时,由于此时 shouldTrack 的值是 false,是停止依赖收集的状态,所以 length 属性与副作用函数之间不会建立响应联系。

3.2 数组元素或属性的设置拦截

通过索引设置数组元素的值时,会执行数组对象的内部方法 [[Set]],而内部方法 [[Set]] 依赖于 [[DefineOwnProperty]]。

根据ECMA规范对数组对象的内部方法[[DefineOwnProperty]]的逻辑定义,如果设置的索引值大于数组当前的长度,那么要更新数组的 length 属性。所以当通过索引设置元素值时,可能会隐式地修改 length 属性。

所以通过索引设置元素值时要触发响应,也应该触发与 length 属性相关联的副作用函数重新执行。

// set操作符的拦截
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    
    // 省略部分代码

    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 original
    // 这里判断receiver是proxy实例才更新派发,防止通过原型链触发拦截器触发更新。
    // target === receiver.raw ,说明 receiver 就是 target 的代理对象
    // 只有当 receiver 是target 的代理对象时才触发更新
    if (target === toRaw(receiver)) {
      // 如果属性不存在,则说明是在添加新的属性,否则是设置已有属性
      if (!hadKey) {
        // 操作类型为 ADD 时,触发响应
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 比较新值与旧值,只有当它们不全等,并且都不是 NaN 的时候才触发响应
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

在 set 拦截函数内,如果代理的目标对象是数组,其被设置的索引值如果小于数组长度,说明被设置的索引值已经存在,则将其视作 SET 操作,因为它不会改变数组长度;如果设置的索引值大于数组的当前长度,则通过 hasOwn 方法判断索引是否存在,如果不存在,则视作 ADD 操作,因为这会隐式第改变数组的 length 属性值。

然后根据 type 的类型和 target 的类型,在 trigger 函数中正确地触发与数组对象的 length 属性相关联的副作用函数重新执行。

// packages/reactivity/src/effect.ts

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    // 目标对象是数组时,取出与length 属性相关联的副作用函数,添加到依赖中
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          // 当操作类型为 ADD 并且目标对象是数组时,应该取出并执行那些与 length 属性相关联的副作用函数
          // 取出与length 属性相关联的副作用函数,添加到依赖中
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  // 省略部分代码
}

4、代理 Set 和 Map

Map 和 Set 这两个数据类型的操作方法相似,它们之间最大的不同体现在,Set 类型使用 add(value) 方法添加元素,Map 类型使用 set(key, value) 方法设置键值对,并且 Map 类型可以使用 get(key) 方法读取相应的值。

4.1 Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set 结构的实例有以下属性:

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

Set 结构的实例有以下操作方法:

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。

Set 结构的实例有四个遍历方法,用于遍历成员:

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员

4.2 Map

Map 结构的实例有以下属性:

  • Map.prototype.constructor:构造函数,默认就是Map函数。
  • Map.prototype.size:返回Map实例的成员总数。

Map 结构的实例有以下操作方法:

  • Map.prototype.get(key):读取key对应的键值,如果找不到key,返回undefined。
  • Map.prototype.set(key, value):设置键名key对应的键值为value,然后返回整个 Map 结构。
  • Map.prototype.delete(key):删除某个键,返回true。如果删除失败,返回false。
  • Map.prototype.has(key):返回一个布尔值,表示某个键是否在当前 Map 对象之中。
  • Map.prototype.clear():清除所有成员,没有返回值。

Map 结构的实例有四个遍历方法,用于遍历成员:

  • Map.prototype.keys():返回键名的遍历器
  • Map.prototype.values():返回键值的遍历器
  • Map.prototype.entries():返回键值对的遍历器
  • Map.prototype.forEach():使用回调函数遍历每个成员

4.3 size 属性的拦截

在拦截 size 属性时需要修正访问器属性的 getter 函数执行时的 this 指向,如下代码:

// packages/reactivity/src/collectionHandlers.ts

// size 属性的拦截
function size(target: IterableCollections, isReadonly = false) {
  // 获取原始对象
  target = (target as any)[ReactiveFlags.RAW]
  // 只有非只读时才收集依赖,响应联系需要建立在 ITERATE_KEY 和副作用函数之间
  !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  // 指定第三个参数 receiver 为原始对象 target,
  // 从而修复拦截 size 属性时访问器属性的 getter 函数执行时的 this 指向问题
  return Reflect.get(target, 'size', target)
}

在上面的 size 属性拦截函数中,第一个参数 target 指向的是代理对象,因此首先需要通过代理对象的 raw 属性获取原始对象。然后在调用 Reflect.get 函数时指定第三个参数为原始对象,这样访问器属性 size 的 getter 函数在执行时,其 this 指向的就是原始对象而非代理对象了。

4.4 get 方法的拦截

// packages/reactivity/src/collectionHandlers.ts

function get(
  target: MapTypes,
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  // #1772: readonly(reactive(Map)) should return readonly + reactive version
  // of the value
  target = (target as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  // 追踪依赖,建立响应联系
  if (key !== rawKey) {
    !isReadonly && track(rawTarget, TrackOpTypes.GET, key)
  }
  // 追踪依赖,建立响应联系
  !isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
  const { has } = getProto(rawTarget)
    // wrap 函数用来把可代理的值转换为响应式数据
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  if (has.call(rawTarget, key)) {
    // 返回包装后的数据
    return wrap(target.get(key))
  } else if (has.call(rawTarget, rawKey)) {
    // 返回包装后的数据
    return wrap(target.get(rawKey))
  } else if (target !== rawTarget) {
    // #3602 readonly(reactive(Map))
    // ensure that the nested reactive `Map` can do tracking for itself
    target.get(key)
  }
}

在 get 拦截函数中,对于代理对象的 key 及原始对象的key 都会收集依赖,建立响应联系。然后通过 target.get 方法获取原始对象的属性值,并调用 warp 函数将其转换成响应式数据并返回。

4.5 set 方法的拦截

// packages/reactivity/src/collectionHandlers.ts

function set(this: MapTypes, key: unknown, value: unknown) {
  // 获取原始数据,由于value本身可能已经是原始数据,所以此时value.raw 不存在,则直接使用 value
  value = toRaw(value)
  // 获取原始对象
  const target = toRaw(this)
  const { has, get } = getProto(target)

  // 先判断要设置的 key 是否存在
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    checkIdentityKeys(target, has, key)
  }

  // 获取旧值
  const oldValue = get.call(target, key)
  // 设置新值
  target.set(key, value)
  //如果不存在,则说明是ADD 类型的操作,意味着新增
  if (!hadKey) {
    trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue)) {
    // 如果存在,并且值变了,则是 SET 类型的操作,意味着修改
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  return this
}

在 set 拦截函数中,其中一个关键点在于首先通过 raw 属性获取原始数据,然后再把原始数据设置到 target 上,这就避免了由于 value 可能是响应式数据而污染原始数据。

而另一个关键点在于,需要判断设置的 key 是否存在,以便区分操作的类型是 SET 还是 ADD。对于 SET 类型和 ADD 类型的操作来说,它们最终触发的副作用函数是不同的。因为 ADD 类型的操作会对数据的 size 属性产生影响。所以任何依赖 size 属性的副作用函数都需要在 ADD 类型的操作发生时重新执行。

在 trigger 函数中,根据type 的类型和 target 的类型,正确地触发与Map 类型相关联的副作用函数重新执行,如下代码所示:

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  let deps: (Dep | undefined)[] = []
  
  // 省略部分代码
  
  else {
    
    // 省略部分代码

    // also run for iteration key on ADD | DELETE | Map.SET
    // 将与 Map 相关的副作用函数从 depsMap 中取出来,添加到依赖集合中
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  // 省略部分代码
}

4.6 has 方法的拦截

// packages/reactivity/src/collectionHandlers.ts

// has 操作符的拦截
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
   // 获取原始对象
  const target = (this as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  // 追踪依赖,建立响应联系
  if (key !== rawKey) {
    !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
  }
  // 追踪依赖,建立响应联系
  !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
  return key === rawKey
    ? target.has(key)
    : target.has(key) || target.has(rawKey)
}

在 has 拦截函数中,也是一样会对代理对象的 key 及原始对象的key 进行依赖收集,建立响应联系。然后调用原始对象的 has 方法来判断一个值是否是 Set 的成员或者一个键是否在 Map 中,并将其结果返回。

4.7 add 方法的拦截

在调用 Set.prototype.add 函数向集合中添加数据时,会间接改变集合的 size 属性,因此,需要在访问size属性时调用 track 函数进行依赖追踪,然后在 add 方法执行时调用 trigger 函数触发响应。size 属性的拦截上文已有介绍,这里我们来看看 add 方法的拦截。

// packages/reactivity/src/collectionHandlers.ts

// add 方法的拦截
function add(this: SetTypes, value: unknown) {
  // 获取原始数据
  value = toRaw(value)
  // add 拦截函数里的 this 仍然指向的是代理对象,通过 raw 属性获取原始数据对象
  const target = toRaw(this)
  const proto = getProto(target)
  // 先判断值是否已经存在
  const hadKey = proto.has.call(target, value)
  // 只有在值不存在的情况下,才需要触发响应
  if (!hadKey) {
    // 通过原始数据对象执行 add 方法添加具体的值
    // 注意,这里不再需要 .bind 了,因为是直接通过原始数据对象 target 调用并执行的
    target.add(value)
    // 调用 trigger 函数触发响应,并指定从操作类型为 ADD
    trigger(target, TriggerOpTypes.ADD, value, value)
  }
  return this
}

在 add 拦截函数中,会先判断值是否已经存在,因为只有在值不存在的情况下,才需要触发响应,这样做对性能更好。值得注意的是,在调用 trigger 函数触发响应时,指定了操作类型为 ADD,这一点很重要。在trigger 函数的实现中,当操作类型是 ADD 或 DELETE 时,会取出与 ITERATE_KEY 相关联的副作用函数执行,这样就可以触发通过访问 size 属性所收集的副作用函数来执行了。

4.8 delete 方法的拦截

// packages/reactivity/src/collectionHandlers.ts

// delete 操作的拦截
function deleteEntry(this: CollectionTypes, key: unknown) {
  // delete 拦截函数里的 this 仍然指向的是代理对象,通过 raw 属性获取原始数据对象
  const target = toRaw(this)
  const { has, get } = getProto(target)
  // 先判断值是否已经存在
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    checkIdentityKeys(target, has, key)
  }

  // 获取旧值
  const oldValue = get ? get.call(target, key) : undefined
  // forward the operation before queueing reactions
  // 通过原始数据对象执行 delete 方法删除具体的值
  // 注意,这里不再需要 .bind 了,因为是直接通过原始数据对象 target 调用并执行的
  const result = target.delete(key)
  // 当要删除的元素确实存在时,才触发响应
  if (hadKey) {
    // 调用 trigger 函数触发响应,并指定操作类型为 DELETE
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  // 返回操作结果
  return result
}

delete 拦截函数里的 this 指向的是代理对象,为了保证通过使用原始数据对象的 delete 方法删除具体的值,首先需要通过代理对象的 raw 属性来获取原始数据对象,然后再调用原始对象的 delete 方法删除具体的值。当要删除的值存在时,调用 trigger 函数触发响应,并指定操作类型为 DELETE。

4.9 clear 方法的拦截

// packages/reactivity/src/collectionHandlers.ts

// clear  操作的拦截
function clear(this: IterableCollections) {
   // clear 拦截函数里的 this 仍然指向的是代理对象,通过 raw 属性获取原始数据对象
  const target = toRaw(this)
  // 判断原始对象中是否还有元素
  const hadItems = target.size !== 0
  const oldTarget = __DEV__
    ? isMap(target)
      ? new Map(target)
      : new Set(target)
    : undefined
  // forward the operation before queueing reactions
  // 通过原始数据对象执行 clear 方法清除所有成员
  const result = target.clear()
  // 还存在元素时才触发响应
  if (hadItems) {
    // 调用 trigger 函数触发响应,并指定操作类型为 CLEAR
    trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
  }
  return result
}

在 clear 拦截函数中,首先通过代理对象的 raw 属性获取原始对象,然后判断原始对象中是否还有元素。在执行原始对象的 clear 方法清除所有成员时,只有原始对象中还存在元素时才调用 trigger 函数触发响应,并指定操作类型为 CLEAR。

4.10 forEach 遍历方法的拦截

// packages/reactivity/src/collectionHandlers.ts

// forEach 遍历的拦截
function createForEach(isReadonly: boolean, isShallow: boolean) {
  return function forEach(
    this: IterableCollections,
    callback: Function,
    thisArg?: unknown
  ) {
    const observed = this as any
    // 获取原始数据对象
    const target = observed[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)

    // wrap 函数用来把可代理的值转换为响应式数据
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    // 与 ITERATE_KEY 建立响应联系
    !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
    // 通过原始数据对象target调用 forEach 方法进行遍历
    return target.forEach((value: unknown, key: unknown) => {
      // important: make sure the callback is
      // 1. invoked with the reactive map as `this` and 3rd arg
      // 2. the value received should be a corresponding reactive/readonly.
      // 手动调用 callback,用 wrap 函数包裹 value 和 key 后再传给 callback,这样就实现了深响应
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
}

遍历操作只与键值对的数量有关,因此任何会修改 Map 对象健值对数量的操作都应该触发副作用函数重新执行,例如 delete 和 add 方法等。所以当 forEach 函数被调用时,应该让副作用函数与 ITERATE_KEY 建立响应联系。因此在 forEach 拦截函数中,调用了 track 函数与 ITERATE_KEY 建立响应联系。

在 forEach 拦截函数中执行原始对象的 forEach 方法,手动调用 callback 函数时,传入了 thisArg 参数来指定 callback 函数执行时的 this,并用 wrap 函数将 value 和 key 包装成响应式数据后再传给 callback,这就保证了执行 forEach 方法时在callback回调函数中拿到的数据都是响应式的。

**forEach 遍历 Map 类型的数据时,即关心键,又关心值。**如果操作类型是 SET ,并且目标对象是 Map 类型的数据,应该触发那些与 ITERATE_KEY 相关联的副作用函数重新执行。如下面 trigger 函数中的代码所示:

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  let deps: (Dep | undefined)[] = []
  
  // 省略部分代码
  
  else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      // 省略部分代码
        
      // 操作类型是 SET ,并且目标对象是 Map 类型的数据,
      // 应该触发那些与 ITERATE_KEY 相关联的副作用函数重新执行
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  // 省略部分代码
}

在上面的代码中,如果操作的目标对象是 Map 类型的数据,则 SET 类型的操作需要触发那些与 ITERATE_KEY 相关联的副作用函数重新执行。

4.11 迭代器方法的拦截

集合类型有三个迭代器方法:

  • entries
  • keys
  • values

调用这些方法会得到相应的迭代器,并且可以使用 for…of 进行循环迭代。由于Map 或 Set 类型本身部署了 Symbol.iterator 方法,因此可以使用 for…of 进行迭代。

在源码中,使用 createIterableMethod 函数实现了 entries、keys、values 三个迭代器方法和 Symbol.iterator 方法的拦截。如下面的代码所示:

// 迭代器方法
function createIterableMethod(
  method: string | symbol,
  isReadonly: boolean,
  isShallow: boolean
) {
  return function (
    this: IterableCollections,
    ...args: unknown[]
  ): Iterable & Iterator {
    // 获取原始数据对象 target
    const target = (this as any)[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    const targetIsMap = isMap(rawTarget)
    const isPair =
      method === 'entries' || (method === Symbol.iterator && targetIsMap)
    const isKeyOnly = method === 'keys' && targetIsMap
    // 获取原始迭代器方法
    // 分别通过 'keys', 'values', 'entries', Symbol.iterator 方法获取原始迭代器方法
    const innerIterator = target[method](...args)
     // wrap 函数用来把可代理的值转换为响应式数据
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    // 调用 track 函数建立响应联系
    // keys 方法只关心 Map 数据的键的变化,因此应该使用 MAP_KEY_ITERATE_KEY 来建立依赖关系
    !isReadonly &&
      track(
        rawTarget,
        TrackOpTypes.ITERATE,
        isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
      )
    // return a wrapped iterator which returns observed versions of the
    // values emitted from the real iterator
    return {
      // iterator protocol
      // 迭代器协议
      next() {
        const { value, done } = innerIterator.next()
        return done
          ? { value, done }
          : {
              // 如果 isPair 不是 undefined,则对 value 进行包装
              // isPair 是 entries 或者是 Symbol.iterator,处理的是键值对 [wrap(value[0]), wrap(value[1])]
              // isPair 是 values 或者是 keys,则处理的是值,即  wrap(value)
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
              done
            }
      },
      // iterable protocol
      // 可迭代协议
      [Symbol.iterator]() {
        return this
      }
    }
  }
}

4.11.1 建立响应联系

在使用 for…of 循环遍历 keys 时 ( for (let key of map.keys()) {} ),Map 类型数据的所有健都没有发生变化,在理想情况下,副作用函数是不应该执行的。因此,在调用 track 函数建立响应联系时,使用 MAP_KEY_ITERATE_KEY 来建立响应联系。而 values、entries、Symbol.iterator 三者则使用 ITERATE_KEY 来建立响应联系。

// 调用 track 函数建立响应联系
// keys 方法只关心 Map 数据的键的变化,因此应该使用 MAP_KEY_ITERATE_KEY 来建立依赖关系
!isReadonly &&
  track(
    rawTarget,
    TrackOpTypes.ITERATE,
    isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
  )

在触发副作用函数重新执行时,当操作类型为 ADD 和 DELETE 类型,除了触发与 ITERATE_KEY 相关联的副作用函数重新执行,还需要触发与 MAP_KEY_ITERATE_KEY 相关联的副作用函数重新执行。如下面 trigger 函数中的代码所示:

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  
  // 省略部分代码
  
  // 操作类型为 ADD 和 DELETE 类型,
  // 除了触发与 ITERATE_KEY 相关联的副作用函数重新执行,
  // 还需要触发与 MAP_KEY_ITERATE_KEY 相关联的副作用函数重新执行

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            // 操作类型为 ADD 时触发Map 数据结构的 keys 方法的副作用函数重新执行
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            // 操作类型为 DELETE 时触发Map 数据结构的 keys 方法的副作用函数重新执行
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }、

  // 省略部分代理
}

4.11.2 返回响应式数据

在使用 for…of 循环遍历 entries、keys、values、Symbol.iterator 时,为了使得获取到的值是响应式的数据,需要将 key 和 value 转换成响应式数据。

在自定义的迭代器实现中,通过原始对象的迭代器方法获取原始迭代器方法,执行原始迭代器的 next 方法获取值 value 以及代表是否结束的 done。如果值不为 undefined,则对其进行包装。

如果迭代器方法是 entries 或者是 Symbol.iterator,那么需要对键值对进行包装,即 [wrap(value[0]), wrap(value[1])]。如果迭代器方法是 values 或者是 keys,那么需要对值进行包装,即 wrap(value),最后返回包装后的代理对象。

const isPair =
method === 'entries' || (method === Symbol.iterator && targetIsMap)
const isKeyOnly = method === 'keys' && targetIsMap
// 获取原始迭代器方法
// 分别通过 'keys', 'values', 'entries', Symbol.iterator 方法获取原始迭代器方法
const innerIterator = target[method](...args)
// wrap 函数用来把可代理的值转换为响应式数据
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive


return {
    // iterator protocol
    // 迭代器协议
    next() {
      const { value, done } = innerIterator.next()
      return done
        ? { value, done }
        : {
            // 如果 isPair 不是 undefined,则对 value 进行包装
            // isPair 是 entries 或者是 Symbol.iterator,处理的是键值对 [wrap(value[0]), wrap(value[1])]
            // isPair 是 values 或者是 keys,则处理的是值,即  wrap(value)
            value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
            done
          }
    },
   
  }

4.11.3 返回可迭代对象

具有 Symbol.iterator() 方法的数据结构称为可迭代对象。例如,数组、字符串、集合等。迭代器是由 Symbol.iterator() 方法返回的对象。

const obj = {
  [Symbol.iterator] : function () {
    return {
      next: function () {
        return {
          value: 1,
          done: true
        };
      }
    };
  }
};

上面代码中,对象obj是可遍迭代的(iterable),因为具有Symbol.iterator属性。执行这个属性,会返回一个迭代器对象。该对象的根本特征就是具有next方法。每次调用next方法,都会返回一个代表当前成员的信息对象,具有value和done两个属性。

可迭代协议:指的是一个对象实现了 Symbol.iterator 方法

迭代器协议:指的是一个对象实现了 next 方法

一个对象可以同时实现可迭代协议和迭代器协议,例如:

const obj = {
  // 迭代器协议
  next() {
    // ...
  }
  
  // 可迭代协议
  [Symbol.iterator]() {
    return this
  }
}

在对 entries、keys、values、Symbol.iterator 的拦截中,返回的对象就实现了可迭代协议和迭代器协议,如下面的代码所示:

return {
  // iterator protocol
  // 迭代器协议
  next() {
    const { value, done } = innerIterator.next()
    return done
      ? { value, done }
      : {
          // 如果 isPair 不是 undefined,则对 value 进行包装
          // isPair 是 entries 或者是 Symbol.iterator,处理的是键值对 [wrap(value[0]), wrap(value[1])]
          // isPair 是 values 或者是 keys,则处理的是值,即  wrap(value)
          value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
          done
        }
  },
  // iterable protocol
  // 可迭代协议
  [Symbol.iterator]() {
    return this
  }
}

5、总结

Vue 3 的响应式数据是基于 Proxy 实现的,Proxy 可以为其它对象创建一个代理对象。所谓代理,指的是对一个对象基本语义的代理,它允许我们拦截并重新定义对一个对象的基本操作。本文分别介绍了 Proxy 对于对象Object、数组Array以及 Map、Set、WeakMap、WeakSet 等集合的代理。

Proxy代理对象的本质,就是从ECMA规范中找到可拦截的基本操作的方法。例如对于属性的读取,通过 get 拦截函数来实现代理,对于in操作符,通过 has 拦截函数实现对 in 操作符的代理。对于设置属性,可以通过 set 拦截函数来实现拦截。对于 delete 操作符,可以使用 deleteProperty 拦截。

使用 Proxy 代理数组时,对于数组元素或属性的读取及设置,仍然可以使用普通对象的拦截函数来拦截。对于数组的查找方法以及栈方法,则是重写这些方法,从而实现拦截。

对于 Map、Set、WeakMap、WeakSet 等集合类型,在使用 Proxy 实现代理时,需要通过重写集合的方法来实现自定义的能力。