Vue3 源码阅读(3)- ProxyHandlers
在上一节 reactive
的源码中,我们发现其向 createReactiveObject
函数传入了两个 handler
:
1 | return createReactiveObject( |
这两个 handler
都是 ProxyHandler
类型,会根据上一节解释过的 TargetType
传给 Proxy
构造函数作为第三个实参:
1 | const proxy = new Proxy( |
mutableHandlers
和 mutableCollectionHandlers
分别为 baseHandlers.ts
,collectionHandlers.ts
暴露的主要监听器
baseHandlers
该文件中的 handler
用于处理 Object
, Array
的代理对象
mutableHandlers
是最主要的实现,上一节 reactive
接口的传参之一,我们直接跳到其源码处来解读:
1 | export const mutableHandlers: ProxyHandler<object> = { |
可以看到,vue3
的响应式系统监听了
赋值
取值
删除属性
判断属性存在
获得对象上存在的属性列表
这 5 种行为
get
跳转倒 get
的实现,发现是一个工厂方法返回的函数:
1 | const get = /*#__PURE__*/ createGetter(); |
其它的 Getter
也都是这个工厂方法构造的:
1 | const get = /*#__PURE__*/ createGetter(); |
再追踪下 createGetter
方法的实现:
1 | function createGetter(isReadonly = false, shallow = false) { |
其接收两个参数,表示该响应式对象是否只读或是浅层的响应式(只有第一层属性是响应式的),默认都是 false
方法返回了 get
函数,用于监听对象的取值行为:
1 | return function get( |
接着来看 get
的实现
首先是对 vue
内部使用的一些 flag
进行特殊处理:
1 | if (key === ReactiveFlags.IS_REACTIVE) { |
ReactiveFlags.RAW
这边存的是原始对象,取原始对象的时候通过对应方法的 Map
来获得其响应式代理对象(上一节 reactive
方法使用的是 reactiveMap
,readonly
使用 readonlyMap
,其它方法以此类推)
代理对象若存在且与当前代理对象相同,说明该对象已经被 proxyHandler
种的方法处理过,直接返回其原对象
然后判断为 Array
数组类型的情况:
1 | const targetIsArray = isArray(target); |
如果为数组且不为只读,并且读取的是 vue
响应式系统需要特殊处理的方法,返回 vue
对这些方法的二次封装,详情请看下一小节
最后直接取值
1 | const res = Reflect.get(target, key, receiver); |
判断一些特殊情况:
key
为Symbol
类型,并且是Symbol
内置的属性;或为不能追踪的属性,直接返回值1
2
3if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res;
}如果属性不是只读的,追踪该属性变动(响应式,后续在
effect
详细说)1
2
3if (!isReadonly) {
track(target, TrackOpTypes.GET, key);
}如果是浅层响应式,直接返回值
1
2
3if (shallow) {
return res;
}如果是
ref
类型的属性,如果不是取数组上的元素,解包返回value
,否则返回值1
2
3
4if (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
9import { 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
6if (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 | function createSetter(shallow = false) { |
接收一个参数,判断是否是浅层的响应式,默认 false
方法返回了 set
函数,用于监听对象的赋值行为:
1 | return function set( |
首先取到旧值
oldValue
1
let oldValue = (target as any)[key];
接着判断旧值为只读且为
ref
时,新值是否也为ref
1
2
3if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false;
}如果不是,直接返回
false
,赋值失败处理不为浅层属性且新值不为只读的情况
1
2
3
4
5
6
7
8
9
10
11
12if (!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
6if (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
时
直接给旧值
ref
的value
赋予新值最后判断是更新还是新属性值
1
2
3
4
5
6
7
8
9
10
11
12
13
14const 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 | import { reactive } from 'vue'; |
删除倒是最简单的,我们直接跳到其实现处:
1 | function deleteProperty(target: object, key: string | symbol): boolean { |
直接删除,如果删除成且该原本是存在的,则触发 DELETE
的响应式操作,最后返回结果
has
has
用来捕捉 in
运算符
只读和浅响应式都无需在使用 in
时进行一些操作,所以实现也是相当简单:
1 | function has(target: object, key: string | symbol): boolean { |
其实就是 get
中的一部分操作,取过值后的跟踪变化操作,并且也要注意不能追踪 Symbol
和 Symbol
上 Symbol
类型的属性
ownKeys
这个方法会捕获获得对象上属性名列表和 Symbol
属性列表的方法 Object.getOwnPropertyNames
,Object.getOwnPropertySymbols
并且在使用 for ... in
操作符的时候也会捕获到
代码实现如下:
1 | function ownKeys(target: object): (string | symbol)[] { |
这边 ITERATE_KEY
的作用是标记该追踪为迭代器类型,一般是在代码或者 SFC
模板语法中的 v-for
写法中用到,又或者是 computed
和 watchEffect
中进行 for ... in
操作时也会用到该标记
在触发 ADD
,DELETE
,SET
等操作时,都会调用该标记中追踪的内容
至于为什么要特殊处理数组的 for ... in
操作去追踪 length
,是因为在issue中,提到了 computed
中进行 for ... in
操作空数组会不更新视图,但是我个人点进去 codepen
并没复现出来…
createArrayInstrumentations
该方法代理了部分可能需要触发响应式操作的数组方法
首先声明一个对象用于存储代理方法:
1 | const instrumentations: Record<string, Function> = {}; |
需监听数组元素变化
然后复写一组需要监听数组元素响应式变化的方法,分别是 includes
, indexOf
, lastIndexOf
:
1 | // instrument identity-sensitive Array methods to account for possible reactive |
来一行行看
首先我们需要在原始对象上操作:
1
const arr = toRaw(this) as any;
如果我们仍然在
this
上操作的话会陷入死循环,大家稍微想下就明白了接着追踪数组上的每一个元素的
GET
操作:1
2
3for (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
12test('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
7const 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
2const arr = reactive<any[]>([{}, {}]);
arr.indexOf(arr[0]); // 正常应该返回 0,如果上面的代码没有额外处理,会返回 -1这边因为响应式系统的原因,
arr[0]
拿到的是一个经过响应式系统处理过的Proxy
对象,那么在原始arr
的原始数组上找肯定找不到,会返回-1
,这种行为是错误的
会影响数组长度的函数
然后是会影响数组长度(会修改数组本身)的函数,有 push
, pop
, shift
, unshift
, splice
:
1 | // instrument length-altering mutation methods to avoid length being tracked |
这边主要是为了处理死循环的问题,这个issue中说的很清楚,是 3.0.0-rc.12
早期版本的问题
复现代码如下:
1 | const arr = reactive([]); |
控制台会不停的输出 1
和 2
但是数组 length
属性又不能不监听,不监听会丢失很多响应性,所以这边处理方法很粗暴:在这些方法执行前暂停追踪,执行后恢复…
最后返回代理方法的集合
其它 handlers
例如 readonly
, shallow
, shallowReadonly
等等,无非就是限制一些行为
比如 readonly
的 set
, deleteProperty
都改为空的实现,get
则只是 createGetter
工厂方法的传参不同罢了
collectionHandlers
该文件中的 handler
用于处理 Map
, Set
, WeakMap
, WeakSet
这类 ES 标准里合集数据结构的代理对象
这类数据对象都是通过自身的方法操作数据,代理对象只需要在 get
监听器中分发需要处理的方法即可
mutableCollectionHandlers
上一节 reactive
接口的传参之一,我们直接跳到其源码处来解读:
1 | export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = { |
正如上面说的,只监听了 get
行为
createInstrumentationGetter
是一个工厂方法,用于构建被代理的方法(e.g. map.get
,set.add
)
createInstrumentationGetter
该方法接收两个参数 isReadonly
, shallow
分别表示是否只读和是否浅层响应:
1 | function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) { |
我们来看里面的实现:
首先根据传参决定用方法的哪种代理实现:
1
2
3
4
5
6
7const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations
: shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations;都为
false
时会取到mutableInstrumentations
接着返回
get
方法:1
2
3
4
5
6
7return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
...
};
再看看该 get
方法的实现:
首先处理取各种 flag 的情况:
1
2
3
4
5
6
7if (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
5return Reflect.get(
hasOwn(instrumentations, key) && key in target ? instrumentations : target,
key,
receiver
);
createInstrumentations
重点来了家人们
该方法构建了方法不同种类的代理实现
具体有这些:
1 | const [ |
mutableInstrumentations
直接看最核心的 mutableInstrumentations
:
1 | const mutableInstrumentations: Record<string, Function> = { |
代理了一大堆方法,来一个个看
get
很多人可能会问,下面这个 this
哪来的:
1 | get(this: MapTypes, key: unknown) { |
其实这是一个 typescript 的特性,为第一个参数、且参数名为 this
的类型定义,用来定义当前函数执行上下文中 this
的类型,并不是外部的形参
vue
中挺多地方使用了 this
但没有检查,所以我们使用 collection 类型的响应式对象时,切记不要下面的代码:
1 | const map = reactive(new Map()); |
下面来看返回的 get
方法的实现,方法接收四个参数:
1 | function get( |
这里要注意 target
指的是响应式对象,而不是原始对象,因为外面传的是 this
接着来看实现:
获得一些原始的值:
1
2
3target = (target as any)[ReactiveFlags.RAW];
const rawTarget = toRaw(target);
const rawKey = toRaw(key);这边把
target
设为了其原始对象然后又获得了其最深层的原始对象(用于处理
readonly
,reactive
套娃的情况)最后获得了其
key
的原始值(因为Map
,Set
的key
可以是引用类型,可能是响应式的)追踪响应式变化:
1
2
3
4
5
6if (!isReadonly) {
if (key !== rawKey) {
track(rawTarget, TrackOpTypes.GET, key);
}
track(rawTarget, TrackOpTypes.GET, rawKey);
}如果只读就不用追踪
如果
key
不等于rawKey
,说明key
为响应式的,所以要追踪key
的变化最后追踪
rawKey
本身的变化这样做是为了用原始对象传参和响应式对象传参的行为都一致:
1
2
3
4
5const 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
11const { 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
5if (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
5else 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
6const map = reactive(new Map()); // 为空
const readonlyMap = readonly(map);
effect(() => readonlyMap.get(1)); // 该副作用应当在 1 上的值变动时调用
map.set(1, 1); // 如果上面的代码去掉,这里 get 后副作用并不会执行这里我也是去掉部分代码后跑测试用例发现的
测试用例作用不仅是测试,更是方便大家学习的好东西(这点 vue 做的很好,糟糕的用例甚至会起到误导作用)
size
这边代理了 size
的 getter
:
1 | get size() { |
看下返回的 size
方法的实现:
1 | function size(target: IterableCollections, isReadonly = false) { |
逻辑很简单
先获得原始对象,若不是只读则追踪迭代操作,最后返回 size
的值
has
has
方法是 Map
用来判断是否存在 key
,或者 Set
用来判断是否存在 value
的方法,方法接收两个参数:
1 | function has( |
分别是 key
和 isReadonly
,this
前面解释过
来看实现
首先获得一些原始对象:
1
2
3const target = (this as any)[ReactiveFlags.RAW];
const rawTarget = toRaw(target);
const rawKey = toRaw(key);追踪响应式变化:
1
2
3
4
5
6if (!isReadonly) {
if (key !== rawKey) {
track(rawTarget, TrackOpTypes.HAS, key);
}
track(rawTarget, TrackOpTypes.HAS, rawKey);
}跟前面
get
的实现完全一样返回值:
1
2
3return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey);判断
key
和rawKey
是否相同,相同直接返回值,否则返回两键查找的或运算结果这样做是为了用原始对象的键和其响应式代理对象查找的结果都一致:
1
2
3
4
5
6
7const 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 | function add(this: SetTypes, value: unknown) { |
来看实现,非常简单
获得原始对象:
1
2value = toRaw(value);
const target = toRaw(this);判断是否已有该值:
1
2
3
4
5
6
7const 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 | function set(this: MapTypes, key: unknown, value: unknown) { |
来看实现
先获得原始对象
1
2
3value = toRaw(value);
const target = toRaw(this);
const { has, get } = getProto(target);还有工具方法的获取
判断是否已经存在该键
1
2
3
4
5
6
7let 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
8const 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
删除指定的值或键,这边 delete
为 deleteEntry
函数:
1 | delete: deleteEntry |
deleteEntry
只在非只读时使用,所以只有一个参数:
1 | function deleteEntry(this: CollectionTypes, key: unknown) { |
来看实现
获得原始值:
1
2const target = toRaw(this);
const { has, get } = getProto(target);顺带获得工具方法
判断是否有该键:
1
2
3
4
5
6
7let 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
7const 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 | function clear(this: IterableCollections) { |
来看实现
获得原始对象:
1
2const target = toRaw(this);
const hadItems = target.size !== 0;顺便记录是否有元素存在
记录旧对象:
1
2
3
4
5const 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 | function createForEach(isReadonly: boolean, isShallow: boolean) { |
其内部返回了一个 forEach
函数,接收两个参数,callback
和 thisArg
:
1 | function forEach( |
来看实现:
获得一些原始对象:
1
2
3const 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
6return 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
原来的行为即可,要把value
和key
经过响应式系统处理
createIterableMethod
然后我们拉到 createInstrumentations
实现的下面,会发现还代理了迭代器相关的方法 keys
, values
, entries
:
1 | const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]; |
它把这些方法的代理方法通过 createIterableMethod
工厂方法构建,并添加到对应的代理合集中:
1 | mutableInstrumentations[method as string] = createIterableMethod( |
createIterableMethod
接收三个参数,method
, isReadonly
, isShallow
:
1 | function createIterableMethod( |
函数返回了一个代理方法:
1 | return function ( |
来看这个方法的实现:
首先获得原始对象和最深层的原始对象:
1
2const target = (this as any)[ReactiveFlags.RAW];
const rawTarget = toRaw(target);跟前面作用一致,处理
readonly
,reactive
套娃的情况判断方法的类型,和处理器类型:
1
2
3
4
5
6const 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
10return {
// 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
9next() {
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 | const readonlyInstrumentations: Record<string, Function> = { |
可以看到都是用之前说过的工厂方法来构建,又或者是方法传参不同
部分不允许的操作用了 createReadonlyMethod
这个工厂方法
createReadonlyMethod
这个方法实现很简单:
1 | function createReadonlyMethod(type: TriggerOpTypes): Function { |
如果是开发模式,则抛出警告,最后直接返回操作失败的返回值