Vue reactive 源码解析
写在最前面
在reactivity
模块中,之所以最后才写reactive
是因为它的原理虽然不复杂,大家都知道是用的proxy
拦截操作,但是实际上代码逻辑比较复杂,对于不同的类型拦截操作不同,笔者花了一点时间读代码和调试,虽然如此,由于能力所限,肯定也不能写全,还望见谅。
reactive 原理综述
reactive
通过proxy
对对象类型(包括对象、数组、集合类型)进行代理,拦截get
、set
、deleteProperty
、has
、ownKeys
操作,在拦截过程中进行依赖的收集和触发,下面分别对原理做简要的介绍:
-
对于对象:
reactive
会拦截对于每个属性的操作。一般来说,当读取对象的某个key
的值的时候,会通过Reflect.get
获得该key
对应的值res
,并调用track
函数收集依赖,如果res
也是对象,则会返回reactive(res)
(为了实现对于对象的深层次的响应式代理),否则返回res
。对于对象属性值的设置会被set
拦截,并通过trigger
函数触发依赖- 其他操作如
in
操作符会被has
拦截,delete
会被deleteProperty
拦截,for...in
和Object.keys()
会被ownKeys
拦截,等等
-
对于数组类型:
-
当操作为类似
arr[0] = 1
时,由于数组为键为"0"、"1"、"2"
的对象,所以对于这种操作,响应式的原理和对象相同 -
对于
for...of
循环,实际上会通过[Symbol.iterator]()
方法获得数组的默认迭代器对象iterator
,并获得iterator.next().value
,这一操作会被proxy
的get
操作拦截。集合类型对象同理 -
当调用数组的
push、pop、shift
等原生方法时,会在数组的get
中被拦截,并调用Vue 3
重载的数组方法。重载方法会触发依赖的更新,并调用原生方法进行对应的操作,这些重载方法写在arrayInstrumentations
中 -
当改变数组的
"length"
属性时,使用到arr.length
的副作用肯定会被触发;对于仅使用到arr[i]
的副作用,需要根据i
和length
的大小关系进行判断:只有当i
大于等于新的length
时,副作用会被重新触发const arr = reactive([1,2,3,4,5,6,7,8]) // 不会被重新触发 watchEffect(() => { console.log(arr[0]) }) // 会被重新触发 watchEffect(() => { console.log(arr[7]) }) arr.length = 3 // 更改 length 属性
-
-
对于集合类型(
Map
、Set
、WeakMap
、WeakSet
),和数组类型的方法拦截相同,Vue
也同样拦截了对于原生方法(如set、get、delete、has、forEach
等方法)的调用,并调用了重载方法,从而触发依赖的更新,这些重载方法的逻辑写在createInstrumentations
函数中
正文
正文部分介绍reactive
的核心逻辑,shallowReactive、readonly
等API
的核心逻辑类似。
在reactive
函数中,会通过createReactiveObject
创建源对象的响应式对象,创建过程的核心部分就是通过代理创建源对象的响应式代理:
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
对于集合类型(COLLECTION
),会使用collectionHandlers
进行代理;对于对象和数组类型会使用baseHandlers
进行代理。
对象和数组类型
对于reactive API
,对象和数组类型使用mutableHandlers
进行代理,对get、set、deleteProperty、has、ownKeys
操作进行了拦截
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
get 操作
get
操作的主要逻辑写在了createGetter
函数返回的get
函数中,解释写在注释中了。
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 访问 target 的一些 vue 自己添加的属性,例如__v_isReactive、__v_isReadonly等
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
}
const targetIsArray = isArray(target)
if (!isReadonly) {
// 如果 target 是数组,并且访问的是 push 等原生方法,那么就返回 vue 重载的方法
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 如果访问的是对象的 hasOwnProperty 方法,则返回 vue 重载的 hasOwnProperty 方法
// 上面重载的是数组方法,这里是重载了对象的方法
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
const res = Reflect.get(target, key, receiver) // 调用 Reflect.get 获得 key 对应的值 res
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
// 只读对象因为无法更改,不具有响应性,所以不需要收集依赖
if (!isReadonly) {
track(target, TrackOpTypes.GET, key) // track 函数收集 target 的 key 依赖
}
if (shallow) {
return res // shallowReactive 不需要具有深层次响应性,直接返回 res 即可
}
if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
// ref 解包,返回 res.value
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.
// 对于 res 也为对象的情况返回 reactive(res) 从而实现深层次响应性
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
set 操作
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
/*
对应情况:
const obj = readonly(ref(0))
const reactiveObj = reactive({ obj })
reactiveObj.obj = 1 // false
*/
return false
}
if (!shallow) {
if (!isShallow(value) && !isReadonly(value)) {
// 获得代理的原始对象
oldValue = toRaw(oldValue)
value = toRaw(value)
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
// 解包的逻辑
/*
对应情况:
const obj = ref(0)
const reactiveObj = reactive({ obj })
reactiveObj.obj = 1 // 相当于更改了 obj.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
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
}
}
deleteProperty & has & ownKeys
function deleteProperty(target: object, key: string | symbol): boolean {
// 删除属性,如果删除成功并且 key 是 target 的自有属性,则触发依赖更新
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
}
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
// 如果 key 不是内置 symbol 类型或 vue 内部使用的 symbol 类型则收集依赖
// 原因见下面 chatgpt 的回答
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key)
}
return result
}
function ownKeys(target: object): (string | symbol)[] {
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
集合类型
这部分的主要任务就是当调用集合类型的原生方法时,改为调用vue
重载的方法
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations
: shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
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(
// 如果访问的属性在 instrumentations 上存在,即访问的是 vue 重载的方法
// 则访问 instrumentations 上实现的重载方法
// 否则返回 target 上的对应属性
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}
总结
reactive
的主要原理是使用proxy
创造原始对象的代理,拦截对于源对象的操作,在该过程中执行依赖的收集和触发,如果调用的是对象的原生方法,则执行vue
重载的方法,从而能够进行在方法中进行依赖的收集和触发。