在上一节 reactive 的源码中,我们发现其向 createReactiveObject 函数传入了两个 handler

1
2
3
4
5
6
7
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
);

这两个 handler 都是 ProxyHandler 类型,会根据上一节解释过的 TargetType 传给 Proxy 构造函数作为第三个实参:

1
2
3
4
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
);

mutableHandlersmutableCollectionHandlers 分别为 baseHandlers.ts,collectionHandlers.ts 暴露的主要监听器

baseHandlers

该文件中的 handler 用于处理 Object, Array 的代理对象

mutableHandlers

是最主要的实现,上一节 reactive 接口的传参之一,我们直接跳到其源码处来解读:

1
2
3
4
5
6
7
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys,
};

可以看到,vue3 的响应式系统监听了

  • 赋值

  • 取值

  • 删除属性

  • 判断属性存在

  • 获得对象上存在的属性列表

这 5 种行为

get

跳转倒 get 的实现,发现是一个工厂方法返回的函数:

1
const get = /*#__PURE__*/ createGetter();

其它的 Getter 也都是这个工厂方法构造的:

1
2
3
4
const get = /*#__PURE__*/ createGetter();
const shallowGet = /*#__PURE__*/ createGetter(false, true);
const readonlyGet = /*#__PURE__*/ createGetter(true);
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true);

再追踪下 createGetter 方法的实现:

1
2
3
function createGetter(isReadonly = false, shallow = false) {
...
}

其接收两个参数,表示该响应式对象是否只读或是浅层的响应式(只有第一层属性是响应式的),默认都是 false

方法返回了 get 函数,用于监听对象的取值行为:

1
2
3
4
5
6
7
return function get(
target: Target,
key: string | symbol,
receiver: object
) {
...
};

接着来看 get 的实现

首先是对 vue 内部使用的一些 flag 进行特殊处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target;
}

ReactiveFlags.RAW 这边存的是原始对象,取原始对象的时候通过对应方法的 Map 来获得其响应式代理对象(上一节 reactive 方法使用的是 reactiveMapreadonly 使用 readonlyMap,其它方法以此类推)

代理对象若存在且与当前代理对象相同,说明该对象已经被 proxyHandler 种的方法处理过,直接返回其原对象

然后判断为 Array 数组类型的情况:

1
2
3
4
5
const targetIsArray = isArray(target);

if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}

如果为数组且不为只读,并且读取的是 vue 响应式系统需要特殊处理的方法,返回 vue 对这些方法的二次封装,详情请看下一小节

最后直接取值

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

判断一些特殊情况:

  • keySymbol 类型,并且是 Symbol 内置的属性;或为不能追踪的属性,直接返回值

    1
    2
    3
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
    return res;
    }
  • 如果属性不是只读的,追踪该属性变动(响应式,后续在 effect 详细说)

    1
    2
    3
    if (!isReadonly) {
    track(target, TrackOpTypes.GET, key);
    }
  • 如果是浅层响应式,直接返回值

    1
    2
    3
    if (shallow) {
    return res;
    }
  • 如果是 ref 类型的属性,如果不是取数组上的元素,解包返回 value,否则返回值

    1
    2
    3
    4
    if (isRef(res)) {
    // ref unwrapping - skip unwrap for Array + integer key.
    return targetIsArray && isIntegerKey(key) ? res : res.value;
    }

    数组上的 ref 元素不解包是为了防止原生的数组方法出现问题,例如 reserve,当然也可以通过复写方法来实现,但是会非常复杂(太多了),参考该 commit,里面提到的 issue 有例子

    目前代码会是这个效果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import { reactive, ref } from 'vue';

    const arr = reactive([ref({ count: 0 })]);
    // 需要手动在 value 上获取
    console.log(arr[0].value.count); // 0

    const obj = reactive({ count: ref(0) });
    // 自动解包
    console.log(obj.count); // 0
  • 到这里可以断定该属性上的值没有被响应式系统处理过,判断是否为对象,进一步处理

    如果为对象,根据该属性是否只读,让响应式系统处理该值

    1
    2
    3
    4
    5
    6
    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.
    return isReadonly ? readonly(res) : reactive(res);
    }

    从这里可以看出 vue 不会一次性把所有值都在响应式系统中处理,嵌套的值只在首次使用后处理,这样性能会很好

  • 最后,不为对象直接返回值本身

    1
    return res;

set

set 方法同上,也是一个工厂方法返回的函数:

1
const set = /*#__PURE__*/ createSetter();

追踪下 createSetter 方法的实现:

1
2
3
function createSetter(shallow = false) {
...
}

接收一个参数,判断是否是浅层的响应式,默认 false

方法返回了 set 函数,用于监听对象的赋值行为:

1
2
3
4
5
6
7
8
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
...
}
  • 首先取到旧值 oldValue

    1
    let oldValue = (target as any)[key];
  • 接着判断旧值为只读ref 时,新值是否也为 ref

    1
    2
    3
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
    return false;
    }

    如果不是,直接返回 false,赋值失败

  • 处理不为浅层属性新值不为只读的情况

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    if (!shallow && !isReadonly(value)) {
    if (!isShallow(value)) {
    value = toRaw(value);
    oldValue = toRaw(oldValue);
    }
    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
    }
    • 如果同时新值还不为浅层响应式对象,则要考虑为响应式代理对象的情况,需确保值原始对象或者普通的值

    为什么旧值新值都需要 toRaw 转换成原始对象呢?

    因为

    在响应式框架中,原始对象上的属性都应当为原始对象或者普通的值,而不能为代理对象(应该是一种设计约定,不处理理论上不会有什么问题)

    获得响应式对象的属性时,应从其 get 监听器中从对应的 proxyMap 中获得值,在 createGetter 最后的一段代码中就是这样处理的:

    1
    2
    3
    4
    5
    6
    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.
    return isReadonly ? readonly(res) : reactive(res);
    }

    返回的两个方法都会根据原始对象的地址去获得已有的响应式对象没有则新建

    所以

    为了维护这个设计约定

    旧值 toRaw 是为了处理默认值为嵌套响应式对象的情况例如 const obj = reactive({ value: reactive({ value: 233 }) });新值 toRaw 为了确保后续不会出现嵌套的情况

    shallow = true 时无需确保,因为这时属性上的值不会在 get 监听器中被改为响应式对象

    • 如果原始对象不为数组且为 ref,并且新值不为 ref

    直接给旧值 refvalue 赋予新值

  • 最后判断是更新还是新属性值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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
    if (target === toRaw(receiver)) {
    if (!hadKey) {
    trigger(target, TriggerOpTypes.ADD, key, value);
    } else if (hasChanged(value, oldValue)) {
    trigger(target, TriggerOpTypes.SET, key, value, oldValue);
    }
    }
    return result;

    给原始对象赋值

    然后这个判断 target === toRaw(receiver),个人推测是单纯的防御性代码,因为理论上不会出现不相等的情况

    接着如果没有值,则触发增加操作,有值则触发赋值操作

    最后返回赋值结果

deleteProperty

监听删除属性的行为,类似下面代码就会触发:

1
2
3
4
import { reactive } from 'vue';

const obj = reactive({ value: 0 });
delete obj.value; // 触发

删除倒是最简单的,我们直接跳到其实现处:

1
2
3
4
5
6
7
8
9
function deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key);
const oldValue = (target as any)[key];
const result = Reflect.deleteProperty(target, key);
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);
}
return result;
}

直接删除,如果删除成且该原本是存在的,则触发 DELETE 的响应式操作,最后返回结果

has

has 用来捕捉 in 运算符

只读和浅响应式都无需在使用 in 时进行一些操作,所以实现也是相当简单:

1
2
3
4
5
6
7
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key);
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key);
}
return result;
}

其实就是 get 中的一部分操作,取过值后的跟踪变化操作,并且也要注意不能追踪 SymbolSymbolSymbol 类型的属性

ownKeys

这个方法会捕获获得对象上属性名列表和 Symbol 属性列表的方法 Object.getOwnPropertyNames,Object.getOwnPropertySymbols

并且在使用 for ... in 操作符的时候也会捕获到

代码实现如下:

1
2
3
4
function ownKeys(target: object): (string | symbol)[] {
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY);
return Reflect.ownKeys(target);
}

这边 ITERATE_KEY 的作用是标记该追踪为迭代器类型,一般是在代码或者 SFC 模板语法中的 v-for 写法中用到,又或者是 computedwatchEffect 中进行 for ... in 操作时也会用到该标记

在触发 ADD,DELETE,SET 等操作时,都会调用该标记中追踪的内容

至于为什么要特殊处理数组的 for ... in 操作去追踪 length,是因为在issue中,提到了 computed 中进行 for ... in 操作空数组会不更新视图,但是我个人点进去 codepen 并没复现出来…

createArrayInstrumentations

该方法代理了部分可能需要触发响应式操作的数组方法

首先声明一个对象用于存储代理方法:

1
const instrumentations: Record<string, Function> = {};

需监听数组元素变化

然后复写一组需要监听数组元素响应式变化的方法,分别是 includes, indexOf, lastIndexOf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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[]) {
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)
const res = arr[key](...args);
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return arr[key](...args.map(toRaw));
} else {
return res;
}
};
});

来一行行看

  • 首先我们需要在原始对象上操作:

    1
    const arr = toRaw(this) as any;

    如果我们仍然在 this 上操作的话会陷入死循环,大家稍微想下就明白了

  • 接着追踪数组上的每一个元素的 GET 操作:

    1
    2
    3
    for (let i = 0, l = this.length; i < l; i++) {
    track(arr, TrackOpTypes.GET, i + '');
    }

    这里很重要,大家可以去掉该循环,然后跑一下测试用例(pnpm test),然后会发现

    reactivity/__tests__/reactiveArray.spec.ts 下第 78 行的测试用例会报错:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    test('Array identity methods should be reactive', () => {
    const obj = {};
    const arr = reactive([obj, {}]);

    let index: number = -1;
    effect(() => {
    index = arr.indexOf(obj);
    });
    expect(index).toBe(0);
    arr.reverse();
    expect(index).toBe(1);
    });

    预期行为是 effect 先执行一次,在 reverse 执行后再执行一次

    indexOf 没有追踪元素变化,则 effect 不会执行,导致最后一句断言抛错

  • 接着就是执行方法本身

    1
    2
    3
    4
    5
    6
    7
    const res = arr[key](...args);
    if (res === -1 || res === false) {
    // if that didn't work, run it again using raw values.
    return arr[key](...args.map(toRaw));
    } else {
    return res;
    }

    为了确保方法正确判断,这边在执行结果为 false, -1 时,在确保参数为原始对象后,会再次调用

    例如下面这种情况:

    1
    2
    const arr = reactive<any[]>([{}, {}]);
    arr.indexOf(arr[0]); // 正常应该返回 0,如果上面的代码没有额外处理,会返回 -1

    这边因为响应式系统的原因,arr[0] 拿到的是一个经过响应式系统处理过的 Proxy 对象,那么在原始 arr 的原始数组上找肯定找不到,会返回 -1,这种行为是错误的

会影响数组长度的函数

然后是会影响数组长度(会修改数组本身)的函数,有 push, pop, shift, unshift, splice

1
2
3
4
5
6
7
8
9
10
// instrument length-altering mutation methods to avoid length being tracked
// which leads to infinite loops in some cases (#2137)
(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach((key) => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking();
const res = (toRaw(this) as any)[key].apply(this, args);
resetTracking();
return res;
};
});

这边主要是为了处理死循环的问题,这个issue中说的很清楚,是 3.0.0-rc.12 早期版本的问题

复现代码如下:

1
2
3
4
5
6
7
8
9
10
11
const arr = reactive([]);

watchEffect(() => {
console.log(1);
arr.push(1);
});

watchEffect(() => {
console.log(2);
arr.push(2);
});

控制台会不停的输出 12

但是数组 length 属性又不能不监听,不监听会丢失很多响应性,所以这边处理方法很粗暴:在这些方法执行前暂停追踪执行后恢复

最后返回代理方法的集合

其它 handlers

例如 readonly, shallow, shallowReadonly 等等,无非就是限制一些行为

比如 readonlyset, deleteProperty 都改为空的实现,get 则只是 createGetter 工厂方法的传参不同罢了

collectionHandlers

该文件中的 handler 用于处理 Map, Set, WeakMap, WeakSet 这类 ES 标准里合集数据结构的代理对象

这类数据对象都是通过自身的方法操作数据,代理对象只需要在 get 监听器中分发需要处理的方法即可

mutableCollectionHandlers

上一节 reactive 接口的传参之一,我们直接跳到其源码处来解读:

1
2
3
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false),
};

正如上面说的,只监听了 get 行为

createInstrumentationGetter 是一个工厂方法,用于构建被代理的方法(e.g. map.get,set.add

createInstrumentationGetter

该方法接收两个参数 isReadonly, shallow 分别表示是否只读和是否浅层响应:

1
2
3
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
...
}

我们来看里面的实现:

  • 首先根据传参决定用方法的哪种代理实现:

    1
    2
    3
    4
    5
    6
    7
    const instrumentations = shallow
    ? isReadonly
    ? shallowReadonlyInstrumentations
    : shallowInstrumentations
    : isReadonly
    ? readonlyInstrumentations
    : mutableInstrumentations;

    都为 false 时会取到 mutableInstrumentations

  • 接着返回 get 方法:

    1
    2
    3
    4
    5
    6
    7
    return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
    ) => {
    ...
    };

再看看该 get 方法的实现:

  • 首先处理取各种 flag 的情况:

    1
    2
    3
    4
    5
    6
    7
    if (key === ReactiveFlags.IS_REACTIVE) {
    return !isReadonly;
    } else if (key === ReactiveFlags.IS_READONLY) {
    return isReadonly;
    } else if (key === ReactiveFlags.RAW) {
    return target;
    }

    没啥好说的,跟 baseHandlers里逻辑一致

    但是这里取 RAW 时的逻辑不一致,导致一些行为上的差异,开了个issue

  • 然后判断是否需要代理其属性,返回

    1
    2
    3
    4
    5
    return Reflect.get(
    hasOwn(instrumentations, key) && key in target ? instrumentations : target,
    key,
    receiver
    );

createInstrumentations

重点来了家人们

该方法构建了方法不同种类的代理实现

具体有这些:

1
2
3
4
5
6
const [
mutableInstrumentations, // reactive
readonlyInstrumentations, // readonly
shallowInstrumentations, // shallow
shallowReadonlyInstrumentations, // shallowReadonly
] = /* #__PURE__*/ createInstrumentations();

mutableInstrumentations

直接看最核心的 mutableInstrumentations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const mutableInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key);
},
get size() {
return size(this as unknown as IterableCollections);
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, false),
};

代理了一大堆方法,来一个个看

get

很多人可能会问,下面这个 this 哪来的:

1
2
3
get(this: MapTypes, key: unknown) {
return get(this, key);
}

其实这是一个 typescript 的特性,为第一个参数、且参数名为 this 的类型定义,用来定义当前函数执行上下文中 this 的类型,并不是外部的形参

vue 中挺多地方使用了 this 但没有检查,所以我们使用 collection 类型的响应式对象时,切记不要下面的代码:

1
2
3
4
5
const map = reactive(new Map());
const getTemp = map.get;
const { get } = map;
getTemp(1); // 会报错
get(1); // 也会报错

下面来看返回的 get 方法的实现,方法接收四个参数:

1
2
3
4
5
6
7
8
function get(
target: MapTypes,
key: unknown,
isReadonly = false,
isShallow = false
) {
...
}

这里要注意 target 指的是响应式对象,而不是原始对象,因为外面传的是 this

接着来看实现:

  • 获得一些原始的值:

    1
    2
    3
    target = (target as any)[ReactiveFlags.RAW];
    const rawTarget = toRaw(target);
    const rawKey = toRaw(key);

    这边把 target 设为了其原始对象

    然后又获得了其最深层的原始对象(用于处理 readonly, reactive 套娃的情况)

    最后获得了其 key 的原始值(因为 Map,Setkey 可以是引用类型,可能是响应式的)

  • 追踪响应式变化:

    1
    2
    3
    4
    5
    6
    if (!isReadonly) {
    if (key !== rawKey) {
    track(rawTarget, TrackOpTypes.GET, key);
    }
    track(rawTarget, TrackOpTypes.GET, rawKey);
    }

    如果只读就不用追踪

    如果 key 不等于 rawKey,说明 key 为响应式的,所以要追踪 key 的变化

    最后追踪 rawKey 本身的变化

    这样做是为了用原始对象传参和响应式对象传参的行为都一致:

    1
    2
    3
    4
    5
    const originObj = {};
    const reactObj = reactive(originObj);
    const map = reactive(new Map());
    map.set(originObj, 233);
    map.get(reactObj); // 233
  • 把取到的值也加进响应式系统中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const { has } = getProto(rawTarget);
    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);
    }

    首先选好处理值的处理器 wrap

    1
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive;

    然后判断在原始对象上是否有该 key 或者 rawKey,有的话就在 target 上调用 get

    1
    2
    3
    4
    5
    if (has.call(rawTarget, key)) {
    return wrap(target.get(key));
    } else if (has.call(rawTarget, rawKey)) {
    return wrap(target.get(rawKey));
    }

    为什么是 target.get 而不是 rawTarget.get 呢?

    因为如果是 readonly(reactive()) 套娃的情况,调用 rawTarget.get 并不能经过里面那一层 get 的代理,也就是说不会执行 track 追踪,它会失去响应性

    那最后为啥还要处理套娃的情况?:

    1
    2
    3
    4
    5
    else if (target !== rawTarget) {
    // #3602 readonly(reactive(Map))
    // ensure that the nested reactive `Map` can do tracking for itself
    target.get(key);
    }

    为什么要通过 target.get 手动追踪一下该 key 上值的变动?上面不是已经有处理了吗!

    那是因为前面都是在有值的前提下处理的:if(has.call(rawRTarget, key)),如果没有值呢?例如下面的代码:

    1
    2
    3
    4
    5
    6
    const map = reactive(new Map()); // 为空
    const readonlyMap = readonly(map);

    effect(() => readonlyMap.get(1)); // 该副作用应当在 1 上的值变动时调用

    map.set(1, 1); // 如果上面的代码去掉,这里 get 后副作用并不会执行

    这里我也是去掉部分代码后跑测试用例发现的

    测试用例作用不仅是测试,更是方便大家学习的好东西(这点 vue 做的很好,糟糕的用例甚至会起到误导作用)

size

这边代理了 sizegetter

1
2
3
get size() {
return size(this as unknown as IterableCollections);
}

看下返回的 size 方法的实现:

1
2
3
4
5
function size(target: IterableCollections, isReadonly = false) {
target = (target as any)[ReactiveFlags.RAW];
!isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY);
return Reflect.get(target, 'size', target);
}

逻辑很简单

先获得原始对象,若不是只读则追踪迭代操作,最后返回 size 的值

has

has 方法是 Map 用来判断是否存在 key,或者 Set 用来判断是否存在 value 的方法,方法接收两个参数:

1
2
3
4
5
6
7
function has(
this: CollectionTypes,
key: unknown,
isReadonly = false
): boolean {
...
}

分别是 keyisReadonlythis 前面解释过

来看实现

  • 首先获得一些原始对象:

    1
    2
    3
    const target = (this as any)[ReactiveFlags.RAW];
    const rawTarget = toRaw(target);
    const rawKey = toRaw(key);
  • 追踪响应式变化:

    1
    2
    3
    4
    5
    6
    if (!isReadonly) {
    if (key !== rawKey) {
    track(rawTarget, TrackOpTypes.HAS, key);
    }
    track(rawTarget, TrackOpTypes.HAS, rawKey);
    }

    跟前面 get 的实现完全一样

  • 返回值:

    1
    2
    3
    return key === rawKey
    ? target.has(key)
    : target.has(key) || target.has(rawKey);

    判断 keyrawKey 是否相同,相同直接返回值,否则返回两键查找的或运算结果

    这样做是为了用原始对象的键和其响应式代理对象查找的结果都一致:

    1
    2
    3
    4
    5
    6
    7
    const map = reactive(new Map());
    const origin = {};
    const reactiveOrigin = reactive(origin);
    map.set(reactiveOrigin, 1);

    map.has(reactiveOrigin); // true
    map.has(origin); // true

add

该方法只在 Set 上有,用于往集合中增加元素,并且只会在不是只读的响应式对象上存在,所以只接收一个参数 value

1
2
3
function add(this: SetTypes, value: unknown) {
...
}

来看实现,非常简单

  • 获得原始对象:

    1
    2
    value = toRaw(value);
    const target = toRaw(this);
  • 判断是否已有该值:

    1
    2
    3
    4
    5
    6
    7
    const proto = getProto(target);
    const hadKey = proto.has.call(target, value);
    if (!hadKey) {
    target.add(value);
    trigger(target, TriggerOpTypes.ADD, value, value);
    }
    return this;

    没有则添加,并触发 ADD 响应式事件

    最后返回 this 即代理对象本身

set

用于设置值,只有 Map 上有,并且也是非只读才可用,所以只有两个参数 key, value

1
2
3
function set(this: MapTypes, key: unknown, value: unknown) {
...
}

来看实现

  • 先获得原始对象

    1
    2
    3
    value = toRaw(value);
    const target = toRaw(this);
    const { has, get } = getProto(target);

    还有工具方法的获取

  • 判断是否已经存在该键

    1
    2
    3
    4
    5
    6
    7
    let hadKey = has.call(target, key);
    if (!hadKey) {
    key = toRaw(key);
    hadKey = has.call(target, key);
    } else if (__DEV__) {
    checkIdentityKeys(target, has, key);
    }

    第一次判断不存在,会 toRaw(key) 后再查一次,确保可能存在的原始对象和响应式对象都查过

  • 设置值并触发响应式事件

    1
    2
    3
    4
    5
    6
    7
    8
    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;

    先设置值

    如果不存在该键,则触发 ADD 响应式事件

    如果存在该键,则触发 SET 响应式事件,并且会传递旧值 oldValue

    最后返回 this 即响应式对象自身

delete

删除指定的值或键,这边 deletedeleteEntry 函数:

1
delete: deleteEntry

deleteEntry 只在非只读时使用,所以只有一个参数:

1
2
3
function deleteEntry(this: CollectionTypes, key: unknown) {
...
}

来看实现

  • 获得原始值:

    1
    2
    const target = toRaw(this);
    const { has, get } = getProto(target);

    顺带获得工具方法

  • 判断是否有该键:

    1
    2
    3
    4
    5
    6
    7
    let hadKey = has.call(target, key);
    if (!hadKey) {
    key = toRaw(key);
    hadKey = has.call(target, key);
    } else if (__DEV__) {
    checkIdentityKeys(target, has, key);
    }

    跟上一小节实现一样

  • 删除键值

    1
    2
    3
    4
    5
    6
    7
    const oldValue = get ? get.call(target, key) : undefined;
    // forward the operation before queueing reactions
    const result = target.delete(key);
    if (hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);
    }
    return result;

    先删除键值

    若该键存在则触发 DELETE 响应式操作

    最后返回删除结果

clear

用于清空所有元素,非只读时能使用,没有参数

1
2
3
function clear(this: IterableCollections) {
...
}

来看实现

  • 获得原始对象:

    1
    2
    const target = toRaw(this);
    const hadItems = target.size !== 0;

    顺便记录是否有元素存在

  • 记录旧对象:

    1
    2
    3
    4
    5
    const oldTarget = __DEV__
    ? isMap(target)
    ? new Map(target)
    : new Set(target)
    : undefined;

    这些代码纯粹为了开发环境调试服务

  • 最后执行清理方法:

    1
    2
    3
    4
    5
    6
    // forward the operation before queueing reactions
    const result = target.clear();
    if (hadItems) {
    trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget);
    }
    return result;

    先执行清理

    然后判断原本是否有元素,如果有则触发 CLEAR 响应式操作

    最后返回清理结果

forEach

遍历元素的方法,这种读操作的方法基本跟上面 get 逻辑差不多,由 createForEach 工厂方法构建:

1
forEach: createForEach(false, false);

createForEach 接收两个参数,分别是 isReadonly, isShallow

1
2
3
function createForEach(isReadonly: boolean, isShallow: boolean) {
...
}

其内部返回了一个 forEach 函数,接收两个参数callbackthisArg

1
2
3
4
5
6
7
function forEach(
this: IterableCollections,
callback: Function,
thisArg?: unknown
) {
...
}

来看实现:

  • 获得一些原始对象:

    1
    2
    3
    const observed = this as any;
    const target = observed[ReactiveFlags.RAW];
    const rawTarget = toRaw(target);

    同时记录了当前代理对象为 observed

  • 选好处理值的处理器 wrap

    1
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive;
  • 如果不是只读,则追踪迭代操作

    1
    !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY);
  • 最后调用 forEach

    1
    2
    3
    4
    5
    6
    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.
    return callback.call(thisArg, wrap(value), wrap(key), observed);
    });

    维持 forEach 原来的行为即可,要把 valuekey 经过响应式系统处理

createIterableMethod

然后我们拉到 createInstrumentations 实现的下面,会发现还代理了迭代器相关的方法 keys, values, entries

1
2
3
4
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator];
iteratorMethods.forEach((method) => {
...
});

它把这些方法的代理方法通过 createIterableMethod 工厂方法构建,并添加到对应的代理合集中:

1
2
3
4
5
6
mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
);
...

createIterableMethod 接收三个参数,method, isReadonly, isShallow

1
2
3
4
5
6
7
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
isShallow: boolean
) {
...
}

函数返回了一个代理方法:

1
2
3
4
5
6
return function (
this: IterableCollections,
...args: unknown[]
): Iterable & Iterator {
...
}

来看这个方法的实现:

  • 首先获得原始对象和最深层的原始对象:

    1
    2
    const target = (this as any)[ReactiveFlags.RAW];
    const rawTarget = toRaw(target);

    跟前面作用一致,处理 readonly, reactive 套娃的情况

  • 判断方法的类型,和处理器类型:

    1
    2
    3
    4
    5
    6
    const targetIsMap = isMap(rawTarget);
    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;

    targetIsMap 判断是否为 Map, WeakMap

    isPair 判断是否是数组类型的值,entries 中回调函数得到的是 [key, value]
    同时 Map 类型的迭代器(map[Symbol.iterator])返回的也是 entries

    isKeyOnly 判断是否仅为键值,只有 Map 有键,Set.keys()Set.values() 返回的为同一个迭代器

    innerIterator 为原始对象上的迭代器

    wrap 根据传参决定如何把迭代器访问的值放入响应式系统,跟前面一样

  • 不是只读,则追踪迭代器事件:

    1
    2
    3
    4
    5
    6
    !isReadonly &&
    track(
    rawTarget,
    TrackOpTypes.ITERATE,
    isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
    );

    这里的 MAP_KEY_ITERATE_KEY 其实跟 ITERATE_KEY 在生产环境下都是空字符串,前面也有很多地方并没有处理这个东西,其实是不用处理的

  • 最后返回一个自定义的迭代器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    return {
    // iterator protocol
    next() {
    ...
    },
    // iterable protocol
    [Symbol.iterator]() {
    return this;
    },
    };

    next 方法大家都知道的,下面详细说

    Symbol.iterator 则是为了还原原生迭代器的行为,执行会返回迭代器自身

    其实还有个东西这边没有还原,Symbol.toStringTag 这个字段应当是个字符串,表示其自身的迭代器类型,具体作用看 MDN文档

    例如:

    1
    2
    3
    4
    5
    6
    7
    // 值为 "Set Iterator"
    new Set()[Symbol.iterator]()[Symbol.toStringTag];

    // 会打印 "[object Set Iterator]"
    console.log(
    Object.prototype.toString.call(new Set(['a', 'b'])[Symbol.iterator]())
    );
  • 迭代器 next 的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    next() {
    const { value, done } = innerIterator.next();
    return done
    ? { value, done }
    : {
    value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
    done,
    };
    }

    首先从原始迭代器上获得 next() 的返回值

    接着就是一连串的条件判断:

    • 如果迭代完成,则直接返回原始值,因为此时 value 必定为 undefined

    • 否则判断值是否为 [key, value] 这样的形式,返回被 wrap 处理后的值

其它 handlers

只是 createInstrumentationGetter 的传参不同,传参不同会使用不同的代理方法

readonlyInstrumentations

readonlyInstrumentations 举例说下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const readonlyInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key, true);
},
get size() {
return size(this as unknown as IterableCollections, true);
},
has(this: MapTypes, key: unknown) {
return has.call(this, key, true);
},
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
forEach: createForEach(true, false),
};

可以看到都是用之前说过的工厂方法来构建,又或者是方法传参不同

部分不允许的操作用了 createReadonlyMethod 这个工厂方法

createReadonlyMethod

这个方法实现很简单:

1
2
3
4
5
6
7
8
9
10
11
12
function createReadonlyMethod(type: TriggerOpTypes): Function {
return function (this: CollectionTypes, ...args: unknown[]) {
if (__DEV__) {
const key = args[0] ? `on key "${args[0]}" ` : ``;
console.warn(
`${capitalize(type)} operation ${key}failed: target is readonly.`,
toRaw(this)
);
}
return type === TriggerOpTypes.DELETE ? false : this;
};
}

如果是开发模式,则抛出警告,最后直接返回操作失败的返回值