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 实现代理时,需要通过重写集合的方法来实现自定义的能力。