本文通过抽取Vue3 reactive源码中的主要部分,来理解其响应式object
的实现方式。本文源码基于这个入口文件:github reactive.ts
源码位置:github reactive.ts
export function reactive(target) {
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
reactive
内部调用了createReactiveObject
函数,并传入mutableHandlers
作为Proxy的handler
源码位置:github reactive.ts
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
return proxy
}
可见reactive
的本质,是将普通的object
转换成了Proxy
。而Proxy
只支持非空的object
,所以reactive
函数也只能用于object
。
const handler = {}
new Proxy({}, handler) // 正常运行
new Proxy(1, handler) // Cannot create proxy with a non-object as target or handler
如果用于原始类型的响应式,应该使用Vue3提供的ref
函数。
源码位置:github baseHandlers.ts
mutableHandlers
和mutableCollectionHandlers
用于new Proxy
的第二个参数,是reactive
函数最核心的逻辑,重点关注其get
和set
属性。
export const mutableHandlers = {
get: createGetter(),
set: createSetter(),
deleteProperty,
has,
ownKeys,
}
源码位置:github baseHandlers.ts
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 里面代码比较长,下面拆成几部分理解
// part 1 - 处理内部变量
// part 2 - 处理数组
// part 3 - 处理其他情况
}
}
part 1 - 处理内部变量
// ts语法
const enum ReactiveFlags {
SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw'
}
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
}
使用reactive()
生成的object,都包含ReactiveFlags
对应的属性,可以输出看下效果:RunJS Demo。
使用shallowReactive()
生成的object,只有根结点的属性是响应式的,这和Vue2中的响应式变量的特性是一样的。参见这个示例:Vue3 shallowReactive
part 2 - 处理数组
const targetIsArray = isArray(target)
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
Proxy为数组,且调用数组原型方法时,其实是先get
到方法名,再用这个方法去set
相应的值,比如下面的代码:
const arr = [{count: 1}, {count: 2}]
const proxyArray = new Proxy(arr, {
get(target, prop, receiver) {
console.log(`get ${prop}`)
return Reflect.get(target, prop, receiver)
},
set(target, prop, receiver) {
console.log(`set ${prop}`)
return Reflect.set(target, prop, receiver)
}
})
proxyArray.push({count: 3})
// 依次输出为
get push
get length
set 2
set length
实际运行效果见:Javascript Proxy an array
part 3 - 处理其他情况
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
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)
}
return res
用reactive
初始化的变量中,如果包含ref
类型的值,则取值时不需要带上.value
。例如:
const count = ref(0)
const state = reactive({count: count})
// 以下两行,都能取到count值
console.log(count.value)
console.log(state.count)
实际运行效果见:Vue3 reactive object with ref
源码位置:github baseHandlers.ts
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// part 1 - 赋值
// part 2 - 触发事件
}
}
part 1 - 赋值
let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
if (!shallow) {
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(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)
part 2 - 解发事件
// 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
只有新增(TriggerOpTypes.ADD
)和设置新值(TriggerOpTypes.SET
)的时候,才会触发(trigger)订阅过的事件
原文链接:https://runjs.work/projects/a082a7c4e4b748a8
原文示例保持更新。