Vue3源码解析(三):如何代理Set和Map数据结构

前言

本节介绍Vue3源码是如何拦截Set和Map数据结构的,参考《Vue.js设计与实现》Vue3的源码。如下给出了回答简介版

简洁版

  1. vue3源码如何代理Set和Map数据结构?
  • 还是利用Set和Map,在get拦截函数中拦截访问操作,在set拦截函数中拦截设置操作
  • 只是代理Set和Map时会出现一些问题,所以还是对Set和Map的方法进行了重新拦截设置
  1. 代理set时,如何避免访问size属性报错?
  • Proxy包装后,会去代理对象身上找size属性,但是代理对象身上没有,就会报错
  • 通过Reflect.get(target, key, target)修改this指向,当访问size属性时去原始对象身上找
  1. vue3源码如何避免污染原始数据
  • 如果把原始对象设置到代理对象身上,会造成访问原始对象也触发响应
  • 这时需要在set函数触发时,通过raw属性获取到原始值,设置给target对象
  • 不管是Map数据结构的set还是Set数据结构的add
  1. 如何拦截forEach操作
  • 使用Proxy进行拦截,当访问forEach方法时会触发get拦截函数进行依赖收集,并对forEach方法进行重写
  • 当map的响应式数据发生新增、删除、修改值时,会触发forEach对应的依赖
  • 使用wrap函数对遍历时访问到的值和键进行递归处理
  • 使用ITERATE_KEY这个Symbol类型的key来作为追踪标识
  1. 如何拦截for of协议、entries、values、keys
  • 部署了Symbol.iterator方法,才可以被for of方法遍历
  • 由于代理对象proxy上没有这个方法,所以必须要重写来执行原始对象身上自己的这个方法,并返回Symbol.iterator方法。注意,这几个方法的拦截都需要重写
  • 如果要拦截entries,还需要重写的对象中返回next迭代器协议。注意可迭代协议是Symbol.iterator方法,迭代器协议是next方法,这两个不一样!
  • values和keys需要调用原始对象身上对应的values方法和keys方法。for of, entries, values都需要在新增 设置 删除触发,keys只需要在新增和删除的时候触发,所以keys要设置新的symobl的key区分开

更多Vue源码文章:

1. Vue3 源码解析(一):响应式数据和副作用函数、计算属性原理、侦听器原理

2. Vue3源码解析(二):响应式原理,如何拦截对象

3. Vue3源码解析(三):响应式原理,如何拦截数组

4. Vue2源码解析(一):响应式原理,如何拦截对象

5. Vue2源码解析(二):响应式原理,如何拦截数组

1 代理Set和Map

代理Set和Map的方式与代理普通对象大体相同,在get时进行track依赖追踪,在set时进行trigger触发对应依赖,但是需要解决一些问题

1.1 解决访问Set的size属性报错

当使用Proxy代理Set,并且访问代理对象的size属性会报错

js 复制代码
let set = new Set([1, 2, 3]);
let p1 = reactive(set, {});
effect(() => {
    console.log(p1.size, "p1.size");
});

报错原因是,访问p1.size属性时,其内部的this会指向p1,并且其内部会检测是否存在[[SetData]]内部槽,由于p1代理对象上不存在,只有Set原始对象上存在,所以会报错。

修改方式是:get访问器函数触发时,如果访问的size属性,在Reflect.get函数里面修改this指向为target,这样就会去原始对象身上找[[SetData]]内部方法,能够找到就解决了报错问题,

diff 复制代码
let proxy1 = new Proxy(set, {
    get(target, key, receiver) {
      if (key === "size") {
+        return Reflect.get(target, key, target);
      }
      return Reflect.get(target, key, receiver);
    },
});

1.2 解决调用set.delete()方法报错

当调用delete方法时报错:

js 复制代码
p1.delete(1);

调用p1.delete调用方法不是访问属,不能像下面这样写,因为delete函数内部的this依然指向proxy代理对象

diff 复制代码
 let proxy1 = new Proxy(set, {
    get(target, key, receiver) {
+      if (key === "size" || key === "delete") {
        return Reflect.get(target, key, target);
      }
      return Reflect.get(target, key, receiver);
    },
  });

应该通过.bind方法修改函数内部调用的this

diff 复制代码
let proxy1 = new Proxy(set, {
    get(target, key, receiver) {
      if (key === "size") {
        return Reflect.get(target, key, target);
      }
+      return target[key].bind(target);
    },
});

现在能修改成功

修改createReactive函数

js 复制代码
function createReactive(obj, isShallow = false, isReadonly = false) {
    return new Proxy(obj, {
      get(target, key, receiver) {
        if (key === "raw") {
          return target;
        }
        // 针对Set数据结构的拦截
        if (key === "size") {
          return Reflect.get(target, key, target);
        }
        // delete走这里
        return target[key].bind(target);
      },
    })
}

之前的调用改为这样

diff 复制代码
let set = new Set([1, 2, 3]);
+let proxy1 = reactive(set);
effect(() => {
    console.log(proxy1.size, "proxy1.size");
});

2 建立响应连接

接下来目标是,当执行代理对象的.add.delete方法时,收集所有访问其size属性的副作用函数并执行

第一步,需要在get拦截函数里面,触发track收集依赖

diff 复制代码
get(target, key, receiver) {
    if (key === "raw") {
      return target;
    }
    if (key === "size") {
      // 触发拦截依赖
+      track(target, ITERATE_KEY);
      return Reflect.get(target, key, target);
    }
    return target[key].bind(target);
},

注意,track传入的key必须是ITERATE_KEY,之前在拦截数组for infor of遍历操作时,增加了这个Symbol键,当时是只要往数组里面增加值或者删除值都会触发这个键对应的副作用函数。现在也用这个键,只要往set里面增加或者删除值,都要触发size重新响应。

你可以这样理解,假设页面有一个模版访问了set.size,当你在js中往set里面增加了值,模版的size也要进行更新,他不涉及具体的key,所以用ITERATE_KEY这个key

接着我们声明一个对象,里面存储set会用到的方法

js 复制代码
const mutableInstrumentations = {
    add(key) {
    },
};

修改get里面的拦截函数,在get拦截函数里面如果访问proxy.set或者proxy.delete,都会执行上面对象里面的方法。 注意,在对Proxy拦截时,这个get的return mutableInstrumentations[key];很关键,后续执行map或者set的任何属性或者方法,都会触发mutableInstrumentations[key]里面对应的函数

diff 复制代码
get(target, key, receiver) {
    if (key === "raw") {
      return target;
    }
    // 针对Set数据结构的拦截
    if (key === "size") {
      track(target, ``);
      return Reflect.get(target, key, target);
    }
    // 这里
+    return mutableInstrumentations[key];
},

add方法实现如下:

js 复制代码
add(key) {
  // 通过.raw属性来访问原始对象
  const target = this.raw;
  // 判断值在不在
  const hadKey = target.has(key);
  // 使用原始对象执行add方法
  const res = target.add(key);
  // 触发trigger响应,指定操作类型为ADD
  if (!hadKey) {
    trigger(target, key, "ADD");
  }
  return res;
},
  • 因为在get拦截函数的末尾,使用的是return mutableInstrumentations[key];没有绑定bind函数,所以add里面的this还是指向proxy代理对象,
  • 直接通过this.raw获取原生对象,执行add操作,同时判断如果已经有这个值,不执行trigger函数触发副作用函数
  • trigger函数传入ADD类型,就会把ITERATE_KEY对应的副作用函数拿出来执行

delete方法的实现如下,和add几乎一样,只是执行target.delete,并且要判断有这个值,才能删除

js 复制代码
delete(key) {
  // 通过.raw属性来访问原始对象
  const target = this.raw;
  // 判断值在不在
  const hadKey = target.has(key);
  // 使用原始对象执行delete方法
  const res = target.delete(key);
  // 值不在时才去触发trigger响应,指定操作类型为DELETE
  if (hadKey) {
    trigger(target, key, "DELETE");
  }
  return res;
},

3 避免污染原始数据

3.1 对Map的set和get方法拦截

如下先实现针对Map数据结构的get和set方法的拦截:

js 复制代码
const mutableInstrumentations = {
    ......
    get (key) {
      const target = this.raw;
      const hadKey = target.has(key);
      // 追踪key建立响应响应的联系
      track(target, key);
      if (hadKey) {
        const res = target.get(key);
        // 如果res是对象,则继续执行reactive递归生成响应式数据
        return typeof res === "object" ? reactive(res) : res;
      }
    },
    set (key, value) {
      // 原始对象
      const target = this.raw;
      // 判断读取的key是否存在
      const had = target.has(key)
      // 旧值
      const oldVal = target[key]
      target.set(key, value)
      // 触发trigger响应,指定操作类型为ADD
      if (!had) {
        // 新增
        trigger(target, key, "ADD");
      } else if (oldVal !== value && (oldVal === oldVal && value === value)) {
        // 修改
        trigger(target, key, "SET");
      }
    }
};

get方法的逻辑和之前的has类似,需要注意:第一需要使用track进行依赖收集,第二如果拿到的结果是对象,则需要进行递归处理继续调用reactive方法

set方法中,需要判断是否有这个值,如果有操作类型就是SET,如果没有操作类型就是ADD

此时实现的效果是,当你在副作用函数里面访问对应的key时,之后在调用set方法,就会再次触发副作用函数

js 复制代码
  let map = new Map([
    ['key', 1],
    ['name', 2],
  ]);
  let proxy = reactive(map);
  effect(() => {
    console.log(proxy.get('key'), "proxy1.key");
  });

我们在控制台操作效果如下,成功打印了key

3.2 数据污染

原始数据污染是指:当我们把响应式数据设置给原始数据对象时,对原始数据对象的修改也会触发响应。目前来看 Proxy的拦截会造成数据污染

js 复制代码
let map2 = new Map();
let p1 = reactive(map2)
let p2 = reactive(map2)
// 为p1设置一个键值对
p1.set('p2', p2)
effect(() => {
    console.log(map2.get('p2').size, 'map2.get p2 . size');
})

如上,p1和p2都是基于map2生成响应式数据,我们进行的操作:

  • 第一,把p2作为值设置给p1;
  • 第二,访问原始对象的p2的size属性,这之后,当我们对原始map对象进行设置时,也触发了副作用函数:

原始对象变成了响应式数据,问题的原因如下:

diff 复制代码
set (key, value) {
  // 原始对象
  const target = this.raw;
  // 判断读取的key是否存在
  const had = target.has(key)
  // 旧值
  const oldVal = target[key]
+  // 这里把value响应式数据原封不动赋值给target了
+  target.set(key, value)
  // 触发trigger响应,指定操作类型为ADD
  if (!had) {
    // 新增
    trigger(target, key, "ADD");
  } else if (oldVal !== value && (oldVal === oldVal && value === value)) {
    // 修改
    trigger(target, key, "SET");
  }
}

应该这样解决:通过raw属性拿到响应式数据的原始值,并将原始数据赋值给target

diff 复制代码
set (key, value) {
  // 原始对象
  const target = this.raw;
  // 判断读取的key是否存在
  const had = target.has(key)
  // 旧值
  const oldVal = target[key]
  // target.set(key, value) // 这样赋值就是变量污染
+  const rawValue = value.raw || value
+  target.set(key, rawValue)
  // 触发trigger响应,指定操作类型为ADD
  if (!had) {
    // 新增
    trigger(target, key, "ADD");
  } else if (oldVal !== value && (oldVal === oldVal && value === value)) {
    // 修改
    trigger(target, key, "SET");
  }
}

如上,如果value是个响应式数据,那么通过.raw能够拿到原始数据。如下操作后就没有触发副作用函数

Set数据结构封装的add方法也会出现响应式数据:

js 复制代码
let set = new Set([1,2,3])
let p3 = reactive(set)
let p4 = reactive(set)

应该如下修改:

diff 复制代码
add(key) {
  // 通过.raw属性来访问原始对象
  const target = this.raw;
  // 判断值在不在
  const hadKey = target.has(key);
  // 使用原始对象执行add方法
+  const rawKey = key.raw || key
+  const res = target.add(rawKey);
  // 触发trigger响应,指定操作类型为ADD
  if (!hadKey) {
    trigger(target, key, "ADD");
  }
  return res;
},

此时原始数据正常了

对应源码如下,我截取了片段

diff 复制代码
/packages/reactivity/src/collectionHandlers.ts

set(this: MapTypes, key: unknown, value: unknown) {
    if (!shallow && !isShallow(value) && !isReadonly(value)) {
+      value = toRaw(value)
    }
    // 获取原始值
    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.call(target, key)
+    target.set(key, value)
    if (!hadKey) {
      trigger(target, TriggerOpTypes.ADD, key, value)
    } else if (hasChanged(value, oldValue)) {
      trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    }
    return this
},

4 处理forEach

拦截forEach的操作如下:

  • 需要将该对象和ITERATE_KEY绑定,因为任何修改map长度的操作都会影响forEach操作
diff 复制代码
const mutableInstrumentations = {
    ......
    forEach (callback) {
        const target = this.raw
        // 与原始值建立响应练习
+        track(target, ITERATE_KEY)
        // 执行原始值的forEach,将回调传过去
        target.forEach(callback)
    }
};

测试代码如下:

js 复制代码
  let map = new Map([
    ['key', 1],
    ['name', 2],
  ]);
  let p1 = reactive(map);
  effect(() => {
    p1.forEach(function (value, key) {
        console.log(value, 'forEach触发了 value');
        console.log(key, 'forEach触发了 value');
    })
  });

4.1 递归处理响应值

当执行p1.get(key).delete(2)时并不会触发副作用函数执行,因为value是Set数据结构,是一个原始数据类型,访问value.size无法建立响应链接

js 复制代码
let key = {key: 1}
let value = new Set([1,2,3])
let map = new Map([
    [key, value],
]);
let p1 = reactive(map);
effect(() => {
    p1.forEach(function (value, key) {
        console.log(value.size, '副作用函数 value.size');
    })
});
p1.get(key).delete(2)

在forEach拦截函数中,将数据深层次递归,转化为响应式。并将最新的this传递过去

diff 复制代码
+forEach (callback, thisArg) {
+    const wrap = (val) => typeof val === 'object' ? reactive(val) : val
    const target = this.raw
    // 与原始值建立响应练习
    track(target, ITERATE_KEY)
    // 执行原始值的forEach,将回调传过去
+    target.forEach((v, k) => {
+        callback.call(thisArg, wrap(v), wrap(k), this)
+    })
}

4.2 区分for in和 forEach遍历

  1. for in遍历只关心键,只有对象数量发生变化,新增和删除才会触发对应副作用函数,修改不会
  2. forEach会访问值,所以当SET操作触发修改值,也应该要触发forEach对应的副作用函数

测试数据如下:

js 复制代码
let map = new Map([
    ['a', 1],
    ['b', 2],
]);
let p1 = reactive(map);
effect(() => {
    p1.forEach(function (value, key) {
        console.log(value, '副作用函数 value');
    })
});

此时修改'a'的值并没有触发forEach遍历,

trigger函数是这样修改:

  • 判断是SET操作,并且是Map数据结构,则应该也要触发对应的副作用函数
diff 复制代码
function trigger(target, key, type, newVal) {
    let depsMap = bucket.get(target);
    if (!depsMap) return;
    // 取得与key相关联的副作用函数
    const effects = depsMap.get(key);
    const effectsToRun = new Set();
    // 将与key相关联的副作用函数添加到effectsToRun
    effects &&
      effects.forEach((effectFn) => {
        if (effectFn !== activeEffect) {
          effectsToRun.add(effectFn);
        }
      });

    // 操作类型是ADD的时候,把length对应的副作用函数取出来,加入到effectsToRun中拿出来执行
    if (Array.isArray(target) && type === "ADD") {
      const lengthOfEffects = depsMap.get("length");
      lengthOfEffects &&
        lengthOfEffects.forEach((effectFn) => {
          if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn);
          }
        });
    }
+    if (type === "ADD" || type === "DELETE" || (type ===  'SET' && +Object.prototype.toString.call(target) === '[object Map]')) {
      // 取得与ItERATE_KEY关联的副作用函数
      const iterateEffects = depsMap.get(ITERATE_KEY);
      // 将与ITERATE_KEY相关联的副作用函数添加到effectsToRun
      iterateEffects &&
        iterateEffects.forEach((effectFn) => {
          if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn);
          }
        });
    }
    // 如果操作目标是数组,并且修改了数组的key属性
    if (Array.isArray(target) && key === "length") {
      depsMap.forEach((effects, effectKey) => {
        if (effectKey >= newVal) {
          effects.forEach((effectFn) => {
            if (effectFn !== activeEffect) {
              effectsToRun.add(effectFn);
            }
          });
        }
      });
    }
    effectsToRun &&
      effectsToRun.forEach((effectFn) => {
        if (effectFn.options && effectFn.options.scheduler) {
          effectFn.options.scheduler(effectFn);
        } else {
          effectFn();
        }
      });
  }

源码如下:

diff 复制代码
// packages/reactivity/src/dep.ts
switch (type) {
    case TriggerOpTypes.ADD:
      if (!targetIsArray) {
        run(depsMap.get(ITERATE_KEY))
        if (isMap(target)) {
          run(depsMap.get(MAP_KEY_ITERATE_KEY))
        }
      } else if (isArrayIndex) {
        // new index added to array -> length changes
        run(depsMap.get('length'))
      }
      break
    case TriggerOpTypes.DELETE:
      if (!targetIsArray) {
        run(depsMap.get(ITERATE_KEY))
        if (isMap(target)) {
          run(depsMap.get(MAP_KEY_ITERATE_KEY))
        }
      }
      break
+        case TriggerOpTypes.SET:
           // 如果是SET操作,并且是Map,执行run触发依赖
+          if (isMap(target)) {
+            run(depsMap.get(ITERATE_KEY))
+          }
+          break
  }
}

5. 迭代器方法

5.1 处理for of

Proxy直接拦截会报错

js 复制代码
let map = new Map([
    ['a', 1],
    ['b', 2],
]);
let p1 = reactive(map);
effect(() => {
    for (const [key, value] of p1.entries()) {
        console.log(key, 'key');
        console.log(value, 'value');
    }
});

当使用for of遍历代理对象,会试图从代理对象身上找迭代器协议Symbol.iterator方法,找不到就报错了。应该重写该方法

js 复制代码
[Symbol.iterator] () {
    // 1. 包装写法1
    // const target = this.raw
    // const itr = target[Symbol.iterator]()
    // return itr

    // 2. 包装写法2
    const target = this.raw
    const itr = target[Symbol.iterator]()
    const wrap = (val) => {
        return typeof val === 'object' && val !== null ? reactive(val) : val 
    }
    
    // 建立响应依赖联系
    track(target, ITERATE_KEY)
    
    return {
        next () {
            // itr.next()的结果是['a', 1]和['b', 2]
            const { value, done} = itr.next()
            return {
                // 非undefined则进行包裹
                value: value ? [wrap(value[0]), wrap(value[1])] : value,
                done
            }
        }
    }
}

测试复杂数据类型的值

js 复制代码
let map = new Map([
    ['a', new Set([1,2,3])],
    ['b', new Set([1,2,3,4])],
]);
let p1 = reactive(map);
effect(() => {
    for (const [key, value] of p1) {
        console.log(value.size, 'value.size');
    }
});

能够触发响应:

5.2 处理entries

首先需要理解一点,在map中map2[Symbol.iterator]map2.entries是等价的:

但是直接拦截p1.entries会报错,如下:

js 复制代码
let map = new Map([
    ['a', new Set([1,2,3])],
    ['b', new Set([1,2,3,4])],
]);
let p1 = reactive(map);
effect(() => {
    for (const [key, value] of p1.entries()) {
        console.log(value.size, 'value.size');
    }
});

原因在于,该返回值有next方法,有迭代器协议,但是没有可迭代协议。迭代器协议就是next方法,而可迭代协议是Symbol.iterator方法(数组、set、map都部署了)。

之前写法中返回了next方法,但是没有返回Symbol.iterator方法。

diff 复制代码
return {
+    next () { // 有next
        // itr.next()的结果是['a', 1]和['b', 2]
        const { value, done} = itr.next()
        return {
            // 非undefined则进行包裹
            value: value ? [wrap(value[0]), wrap(value[1])] : value,
            done
        }
    }
+   Symbol.iterator() {} // 还应该有这个
}

修改如下:

diff 复制代码
return {
    // 迭代器协议
    next () {
        // itr.next()的结果是['a', 1]和['b', 2]
        const { value, done} = itr.next()
        return {
            // 非undefined则进行包裹
            value: value ? [wrap(value[0]), wrap(value[1])] : value,
            done
        }
    },
    // 可迭代协议
+    [Symbol.iterator] () {
+        return this // this返回的是代理对象
+    }
}

备注:在set中,这两个是不等价的,如下图所示

5.3 处理values和keys方法

代理values方法,如下增加valueMethod方法,

diff 复制代码
function valueMethod () {
    const target = this.raw
+    const itr = target.values()
    const wrap = (val) => {
        return typeof val === 'object' && val !== null ? reactive(val) : val 
    }

    // 建立响应依赖联系
    track(target, ITERATE_KEY)

    return {
        // 迭代器协议
        next () {
            
            const { value, done} = itr.next()
            return {
                // 非undefined则进行包裹
+                value: wrap(value), // // value的值是1 2
                done
            }
        },
        // 可迭代协议
        [Symbol.iterator] () {
            return this
        }
    }
  }

拦截keys方法

diff 复制代码
function keyMethod () {
    const target = this.raw
+    const itr = target.keys()
    const wrap = (val) => {
        return typeof val === 'object' && val !== null ? reactive(val) : val 
    }

    // 建立响应依赖联系
    track(target, ITERATE_KEY)

    return {
        // 迭代器协议
        next () {
            // itr.next()的结果是['a', 1]和['b', 2]
            const { value, done} = itr.next()
            return {
                // 非undefined则进行包裹
+                value: wrap(value),
                done
            }
        },
        // 可迭代协议
        [Symbol.iterator] () {
            return this
        }
    }
}

修改拦截对象:

diff 复制代码
const mutableInstrumentations = {
    ......
    [Symbol.iterator]: iterationMethod,
+    entries: iterationMethod,
+    values: valueMethod,
+    keys: keyMethod
};

测试数据

js 复制代码
  let map = new Map([
    ['a', 1],
    ['b', 2],
  ]);
  let p1 = reactive(map);
  effect(() => {
    for (const value of p1.values()) {
        console.log(value, 'value');
    }
  });
  effect(() => {
    for (const key of p1.keys()) {
        console.log(key, 'key');
    }
  });

效果:

5.4 解决keys存在的问题

目前来看,修改map的值也会触发keys对应的副作用函数执行,但是key的数量是没有增加或者减少的,不应该触发

解决方式是:

keys拦截用新的key

diff 复制代码
+const MAP_KEY_ITERATOR_KEY = Symbol()
function keyMethod () {
    const target = this.raw
    const itr = target.keys()
    const wrap = (val) => {
        return typeof val === 'object' && val !== null ? reactive(val) : val 
    }

    // 建立响应依赖联系
+    track(target, MAP_KEY_ITERATOR_KEY)

    return {
        // 迭代器协议
        next () {
            // itr.next()的结果是['a', 1]和['b', 2]
            const { value, done} = itr.next()
            return {
                // 非undefined则进行包裹
                value: wrap(value),
                done
            }
        },
        // 可迭代协议
        [Symbol.iterator] () {
            return this
        }
    }
}

只有新增和删除,执行这个key对应的副作用函数,这样当SET操作时不会触发keys拦截对应的副作用函数啦

diff 复制代码
function trigger(target, key, type, newVal) {
    ......
    // map的keys拦截,只有add和delete时才能触发,走这里
+    if ((type === "ADD" || type === "DELETE") && (Object.prototype.toString.call(target) +=== '[object Map]')) {
+      // 取得与ItERATE_KEY关联的副作用函数
+      const iterateEffects = depsMap.get(MAP_KEY_ITERATOR_KEY);
+      // 将与ITERATE_KEY相关联的副作用函数添加到effectsToRun
+      iterateEffects &&
+        iterateEffects.forEach((effectFn) => {
+          if (effectFn !== activeEffect) {
+            effectsToRun.add(effectFn);
+          }
+        });
+    }
    // 如果是set类型则触发这里`ITERATE_KEY`
    if (type === "ADD" || type === "DELETE" || (type ===  'SET' && Object.prototype.toString.call(target) === '[object Map]')) {
      // 取得与ItERATE_KEY关联的副作用函数
      const iterateEffects = depsMap.get(ITERATE_KEY);
      // 将与ITERATE_KEY相关联的副作用函数添加到effectsToRun
      iterateEffects &&
        iterateEffects.forEach((effectFn) => {
          if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn);
          }
        });
    }
    ......
}

请看源码

diff 复制代码
// packages/reactivity/src/collectionHandlers.ts
function createIterableMethod(
  method: string | symbol,
  isReadonly: boolean,
  isShallow: boolean,
) {
  return function (
    this: IterableCollections,
    ...args: unknown[]
  ): Iterable<unknown> & Iterator<unknown> {
    const target = this[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    const targetIsMap = isMap(rawTarget)
    // 在这里区分entries values kyes方法
+    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, // 如果是keys方法,则执行MAP_KEY_ITERATE_KEY的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 }
          : {
+              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), // 区分(entries)和(values、keys)
              done,
            }
      },
      // iterable protocol
+      [Symbol.iterator]() { // 封装的可迭代协议
        return this
      },
    }
  }
}
相关推荐
知否灬知否8 分钟前
Vue3+elementPlus中 树形控件封装
前端·javascript·vue.js
Sailing9 分钟前
🤔🤔你监听 DOM 的方式,可能正在悄悄拖垮性能!
前端·javascript·面试
阿珊和她的猫13 分钟前
React Hooks 的使用
前端·react.js·状态模式
猿大师播放器16 分钟前
如何在新版谷歌Chrome上加载IE的Activex/OCX控件?
前端·chrome·web
林太白24 分钟前
Nest如何连接数据库
前端·后端·node.js
爱因斯坦乐28 分钟前
【HTML】动态背景效果前端页面
前端·javascript·css·html·js
洛小豆36 分钟前
深入剖析 JavaScript 循环变量作用域:let 与 var 的差异
前端·javascript·面试
独立开阀者_FwtCoder40 分钟前
【总结】2326- 资源预加载可能会拖慢网站速度
前端·javascript·vue.js
LAY家的奶栗子是德云女孩1 小时前
HTML5+CSS前端开发【保姆级教学】+超链接标签
前端·css·笔记·html5
独立开阀者_FwtCoder1 小时前
Vite 的实现原理,确实很巧妙
前端·javascript·面试