共计 10021 个字符,预计需要花费 26 分钟才能阅读完成。
【vue3 源码】八、reactive——Collection 的响应式实现
参考代码版本:vue 3.2.37
官网文档:https://vuejs.org/
前文中咱们剖析了
reactive
对Object
类型的数据处理,这篇文章持续介绍对汇合的解决。
mutableCollectionHandlers
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}
对于汇合,读取操作和批改操作都是通过调用办法(size
除外)进行,所以只须要捕捉其 get
办法即可。get
捕捉器通过 createInstrumentationGetter
函数生成。
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations
: shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
// 解决非凡的 key
if (key === ReactiveFlags.IS_REACTIVE) {return !isReadonly} else if (key === ReactiveFlags.IS_READONLY) {return isReadonly} else if (key === ReactiveFlags.RAW) {return target}
return Reflect.get(hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}
createInstrumentationGetter
函数接管两个参数:isReadonly
(是否是只读响应式)、shallow
(是否是浅层响应式)。
首先依据 isReadonly
和shallow
的值,获取对应的instrumentations
。
const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations
: shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
instrumentations
是什么呢?
instrumentations
就是个对象,它通过 createInstrumentations
生成,外部重写了汇合的一些办法。createInstrumentations
办法创立四个instrumentations
:
mutableInstrumentations
:解决可批改的响应式汇合数据readonlyInstrumentations
:解决只读的响应式汇合数据shallowInstrumentations
:解决浅层响应式汇合数据shallowReadonlyInstrumentations
:解决浅层只读响应式汇合数据
const [
mutableInstrumentations,
readonlyInstrumentations,
shallowInstrumentations,
shallowReadonlyInstrumentations
] = /* #__PURE__*/ createInstrumentations()
mutableInstrumentations
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
get
函数可接管四个参数:target
指标汇合、key
、isReadonly
是否是只读响应式、isShallow
是否是浅层响应式。
function get(
target: MapTypes,
key: unknown,
isReadonly = false,
isShallow = false
) {// readonly(reactive(Map)) 应该返回值的 readonly + reactive
target = (target as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
// 如果 key 与其原始对象不统一,阐明 key 是响应式数据
if (key !== rawKey) {
// 如果不是只读的话,收集 key 的依赖
!isReadonly && track(rawTarget, TrackOpTypes.GET, key)
}
// 收集 key 的原始值的依赖
!isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
const {has} = getProto(rawTarget)
// 确定包装函数
// 如果是浅层响应式,包装函数返回入参:(value) => value
// 如果是只读的响应式,包装函数就是会将 value 转为 readonly
// 否则应用 reactive 将 value 转为 reactive
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
// target 的原始值本身蕴含 key 值
if (has.call(rawTarget, key)) {return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) { // target 的原始值本身蕴含 key 的原始值
return wrap(target.get(rawKey))
} else if (target !== rawTarget) {// target 与原始值不同, 阐明 target 是个响应式数据,那么持续调用 target.get。例如 readonly(reactive(Map))
target.get(key)
}
}
能够发现,如果通过 get 办法获取一个响应式数据对应的值时,会有两次依赖的收集,为什么这么做呢?
其实这样做的目标是使通过响应式数据的原始值设置 Map
时,可能照常触发依赖。例如上面这个例子:
const key = ref('a')
const map = reactive(new Map())
map.set(key, 'a')
effect(() => {console.log(map.get(key))
})
map.set(key, 'b')
map.set(toRaw(key), 'c')
以上代码会顺次打印 a b c
。无论通过key
还是 key
的原始值进行批改Map
,都可能触发依赖。
size
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)
}
has
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {const target = (this as any)[ReactiveFlags.RAW]
// 取 target/key 的原始值
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
// 如果 key 是响应式对象,收集依赖
if (key !== rawKey) {!isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
}
// 收集依赖 key 的原始值对应依赖
!isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
}
add
function add(this: SetTypes, value: unknown) {
// 获取 value 与 this 的原始对象
value = toRaw(value)
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
// target 中不存在 value 时,能力触发依赖
if (!hadKey) {target.add(value)
trigger(target, TriggerOpTypes.ADD, value, value)
}
// 返回 this,因为 Set 的 add 操纵能够链式操作
return this
}
set
function set(this: MapTypes, key: unknown, value: unknown) {value = toRaw(value)
const target = toRaw(this)
const {has, get} = getProto(target)
// 先查看 target 中是否存在 key,无论 key 值是不是响应数据
// 如果不存在,再查看是否存在 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)
// 如果不存在 key 阐明是新增操作,反之为批改操作
if (!hadKey) {trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
// 返回 this,因为 Map 的 set 操纵能够链式操作
return this
}
为什么两次查看 key 的存在?
保障应用响应式数据作为 key 向 Map 中增加数据和应用响应式数据的原始值作为 key 向 Map 中批改数据时,批改的是同一个 key 的数据。例如上面这个例子
const key = reactive({})
const map = reactive(new Map())
map.set(toRaw(key), 'c')
map.set(key, 'b')
console.log(map.size) // 1
delete
function deleteEntry(this: CollectionTypes, key: unknown) {const target = toRaw(this)
const {has, get} = getProto(target)
let hadKey = has.call(target, key)
// 如果 target 中没有 key,再寻找是否有 key 的原始值,与 set 雷同
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
// 进行删除
const result = target.delete(key)
if (hadKey) {trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
clear
function clear(this: IterableCollections) {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
const result = target.clear()
// 如果汇合中本就没有值,clear 操作不会触发依赖
if (hadItems) {trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
}
return result
}
forEach
forEach
函数由 createForEach
创立。createForEach
接管两个参数:isReadonly
(是否是只读响应式)、isShallow
(是否是浅层响应式)。
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)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
// 包装 value 及 key,使在 forEach 中拜访到的 key 与 value 的响应性质与 this 保持一致
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
}
shallowInstrumentations
shallowInstrumentations
与 shallowInstrumentations
类似,只不过在生成 get
和createForEach
函数时,传递的参数不一样。
const shallowInstrumentations: Record<string, Function> = {get(this: MapTypes, key: unknown) {return get(this, key, false, true)
},
get size() {return size(this as unknown as IterableCollections)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, true)
}
readonlyInstrumentations
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)
}
readonlyInstrumentations
对象是用来解决只读响应式数据的,所以所有可批改汇合的操作都会通过操作失败。这些能够批改汇合的操作函数都会被一个 createReadonlyMethod
函数生成。
createReadonlyMethod
函数接接管一个 type
参数,并返回一个匿名函数。
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
}
}
shallowReadonlyInstrumentations
const shallowReadonlyInstrumentations: Record<string, Function> = {get(this: MapTypes, key: unknown) {return get(this, key, true, 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, true)
}
除了重写了以上几个办法外,还对 keys
、values
等办法也进行了重写:
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
)
readonlyInstrumentations[method as string] = createIterableMethod(
method,
true,
false
)
shallowInstrumentations[method as string] = createIterableMethod(
method,
false,
true
)
shallowReadonlyInstrumentations[method as string] = createIterableMethod(
method,
true,
true
)
})
keys
、values
、entries
、Symbol.iterator
的重写函数均通过一个 createIterableMethod
函数生成。
Symbol.iterator 是什么?
汇合的 Symbol.iterator
函数能够用来获取迭代器对象,正是因为汇合实现了 Symbol.iterator
办法,所以能够应用 for...of
进行迭代。而这里须要重写 Symbol.iterator
办法,目标是为了实现应用 for...of
迭代代理对象。如下:
const map = reactive(new Map([['a', 1], ['b', 2]]))
for(const [key, value] of map) {console.log(key, value)
}
createIterableMethod
接管三个参数:method
、isReadonly
、isShallow
。
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
isShallow: boolean
) {
return function (
this: IterableCollections,
...args: unknown[]): Iterable & Iterator {const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const targetIsMap = isMap(rawTarget)
// 依据 isPair 判断迭代时的参数
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
)
// 返回一个同时满足迭代器协定和可迭代协定的对象
return {
// 迭代器协定
next() {
// 调用原始对象的迭代器的 next 办法获取 value 与 done
const {value, done} = innerIterator.next()
return done
? {value, done}
: {
// 包装 key、value
value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
done
}
},
// 实现可迭代协定,意味着能够应用 for...of 迭代 map.keys/values/entries()
[Symbol.iterator]() {return this}
}
}
}
总结
reactive
通过 Proxy
代理原始对象,通过拦挡代理对象的操作进行依赖的收集与触发。当对代理对象进行读取操作时,进行依赖的收集;对代理对象进行批改操作则触发依赖,无论是读取操作还是批改操作,其实都是操作的原始对象,为了在执行批改操作时不净化原始对象,都会先调用 toRaw
获取 value
的原始值,而后再进行批改。
reactive
的实现是懈怠的,如果不拜访代理对象的属性,那么永远不会将代理对象的属性转为代理对象。
reactive
流程: