阅读视图

发现新文章,点击刷新页面。

Vue3源码解析(四):ref原理与原始值的响应式方案

本文介绍了vue3中的ref的实现原理,还介绍了响应丢失(toRef、toRefs)的情况,以及自动脱ref是如何实现的,参考《Vue.js设计与实现》 更多Vue源码文章:

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

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

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

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

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

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

快速回答版

  1. 介绍vue当中的ref的作用和内部原理
  • 将原始数据类型如数字、字符串、布尔值等转化为响应式数据
  • 内部实现是用一个对象包裹原始数据,属性为value,将对象传递给reactive函数
  • ref能够解决响应丢失问题,toRefs和toRef函数内部的实现原理和ref一致,借助了get拦截器
  1. vue3如何处理自动脱ref
  • 自动脱ref的定义:当访问ref响应式数据时,希望能够直接使用数据,不通过.value,在模版中常见
  • 实现方式:
    • vue3封装了proxyRefs函数,判断数据如果是ref数据(借助__v_isRef标识),则直接返回他的.value的内容。
    • setup里面返回的变量会自动传递给这个函数
  • reactive也能够实现自动脱ref

1. ref原理

Proxy无法对原始数据类型(包括number string boolean null undefined bigInt symbol)做代理,所以ref的实现必须得嵌套一层对象。

其内部实现如下:

function ref (val) {
    const wrapper = {
        value: val
    }
    Object.defineProperty(wrapper, '__v_ifRef', {
        value: true
    })
    return reactive(wrapper)
}
  • 第一,必须用wrapper包裹val的值,属性是value,将该值传递给reactive函数转化为响应式数据。这也是使用ref的数据必须用.value来访问的原因
  • 给wrapper创建了一个__v_ifRef属性,用来区分是原始数据类型还是引用数据类型

2. 解决响应丢失问题

如下解构obj对象的值赋值给newObj对象,并在副作用函数中访问,期待修改obj.foo为10以后副作用函数重新执行,但是如下并不能执行。因为newObj只是一个普通的对象,不会建立响应联系

const obj = reactive({
    foo: 1,
    bar: 2
})
const newObj = {
    ...obj
}
effect(() => {
    console.log(newObj.foo, 'newObj.foo');
});
obj.foo = 10

toRef函数toRefs函数内部实现原理:

  • toRef的返回值就是ref,借助ref来实现响应丢失问题
  • toRefs是批量调用toRef
function toRef(obj, key) {
    const wrapper = {
        get value () {
            // 访问器函数里面,访问obj[key],访问obj这个proxy对象的某个属性
            return obj[key]
        } 
    }
    return wrapper
}
function toRefs(obj) {
    const ret = {}
    // 针对每个属性调用toRef,整个对象都转化为响应式
    for (const key in obj) {
        ret[key] = toRef(obj, key)
    }
    return ret
}

此时修改数据能够触发响应

const obj = reactive({
    foo: 1,
    bar: 2
})
const newObj = {
    ...toRefs(obj)
}
effect(() => {
    console.log(newObj.foo.value, 'newObj.foo');
});

3. 自动脱ref

定义:定义一个ref响应式数据,在某些场合希望能够使用数据不用通过.value

举例:

  • 模版当中,直接访问newObj.foo,而不是newObj.foo.value
  • reactive包裹一个ref数据,也能够给他脱ref

这样写就会很麻烦

<p>{{ newObj.foo.value }}</p>

希望能够这样写

<p>{{ newObj.foo }}</p>

reactive函数的脱ref能力:

const count = ref(0)
const obj = reactive({ count })
obj.count // 0

实现原理:

  • vue3会把setup函数里面返回的响应式数据传递给proxyRefs函数,进行自动脱ref
  • 借助__v_isRef标识来判断是否是响应式数据
function proxyRefs (target) {
    return new Proxy(target, {
        get (target, key, receiver) {
            const value = Reflect.get(target, key, receiver)
            return value.__v_isRef ? value.value : value
        }
    })
}

4. 请看源码

/packages/reactivity/src/ref.ts
export function ref(value?: unknown) {
  return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T = any> {
  _value: T
  private _rawValue: T

  dep: Dep = new Dep()

  public readonly [ReactiveFlags.IS_REF] = true
  public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false

  constructor(value: T, isShallow: boolean) {
    this._rawValue = isShallow ? value : toRaw(value)
+    this._value = isShallow ? value : toReactive(value) // 这里调用了toReactive
    this[ReactiveFlags.IS_SHALLOW] = isShallow
  }
  ……省略下面的代码
}

/packages/reactivity/src/reactive.ts
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

proxyRefs函数

export function proxyRefs<T extends object>(
  objectWithRefs: T,
): ShallowUnwrapRef<T> {
  return isReactive(objectWithRefs)
    ? objectWithRefs
+    : new Proxy(objectWithRefs, shallowUnwrapHandlers) // 请关注这个shallowUnwrapHandlers
}


const shallowUnwrapHandlers: ProxyHandler<any> = {
  get: (target, key, receiver) =>
    key === ReactiveFlags.RAW
      ? target
+      : unref(Reflect.get(target, key, receiver)), // 这里执行了unref方法
  set: (target, key, value, receiver) => {
    const oldValue = target[key]
    if (isRef(oldValue) && !isRef(value)) {
      oldValue.value = value
      return true
    } else {
      return Reflect.set(target, key, value, receiver)
    }
  },
}

export function unref<T>(ref: MaybeRef<T> | ComputedRef<T>): T {
+  return isRef(ref) ? ref.value : ref // 如果是ref数据,直接返回他.value的值
}

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属性会报错

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

image.png

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

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

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方法时报错:

p1.delete(1);

image.png

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

 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

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

现在能修改成功

image.png

修改createReactive函数

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);
      },
    })
}

之前的调用改为这样

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

2 建立响应连接

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

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

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会用到的方法

const mutableInstrumentations = {
    add(key) {
    },
};

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

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方法实现如下:

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,并且要判断有这个值,才能删除

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;
},

image.png

3 避免污染原始数据

3.1 对Map的set和get方法拦截

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

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方法,就会再次触发副作用函数

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

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

image.png

3.2 数据污染

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

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对象进行设置时,也触发了副作用函数:

image.png

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

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

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能够拿到原始数据。如下操作后就没有触发副作用函数

image.png

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

let set = new Set([1,2,3])
let p3 = reactive(set)
let p4 = reactive(set)

image.png

应该如下修改:

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;
},

此时原始数据正常了

image.png

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

/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操作
const mutableInstrumentations = {
    ……
    forEach (callback) {
        const target = this.raw
        // 与原始值建立响应练习
+        track(target, ITERATE_KEY)
        // 执行原始值的forEach,将回调传过去
        target.forEach(callback)
    }
};

测试代码如下:

  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');
    })
  });

image.png

4.1 递归处理响应值

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

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传递过去

+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)
+    })
}

image.png

4.2 区分for in和 forEach遍历

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

测试数据如下:

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

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

trigger函数是这样修改:

  • 判断是SET操作,并且是Map数据结构,则应该也要触发对应的副作用函数
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();
        }
      });
  }

源码如下:

// 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直接拦截会报错

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');
    }
});

image.png

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

[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
            }
        }
    }
}

测试复杂数据类型的值

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');
    }
});

能够触发响应:

image.png

5.2 处理entries

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

image.png

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

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');
    }
});

image.png

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

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

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() {} // 还应该有这个
}

修改如下:

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中,这两个是不等价的,如下图所示

image.png

5.3 处理values和keys方法

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

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方法

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
        }
    }
}

修改拦截对象:

const mutableInstrumentations = {
    ……
    [Symbol.iterator]: iterationMethod,
+    entries: iterationMethod,
+    values: valueMethod,
+    keys: keyMethod
};

测试数据

  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');
    }
  });

效果:

image.png

5.4 解决keys存在的问题

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

image.png

解决方式是:

keys拦截用新的key

+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拦截对应的副作用函数啦

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);
          }
        });
    }
    ……
}

请看源码

// 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
      },
    }
  }
}
❌