【vue3源码】七、reactive——Object的响应式实现
参考代码版本:vue 3.2.37
官方文档:https://vuejs.org/
reactive
返回一个对象的响应式代理。
使用
const obj = {
count: 1,
flag: true,
obj: {
str: ''
}
}
const reactiveObj = reactive(obj)
源码解析
reactive
export function reactive(target: object) {
// 如果target是个只读proxy,直接return
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
reactive
首先判断target
是不是只读的proxy
,如果是的话,直接返回target
;否则调用一个createReactiveObject
方法。
createReactiveObject
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler,
collectionHandlers: ProxyHandler,
proxyMap: WeakMap
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
createReactiveObject
接收五个参数:target
被代理的对象,isReadonly
是不是只读的,baseHandlers
proxy的捕获器,collectionHandlers
针对集合的proxy捕获器,proxyMap
一个用于缓存proxy的WeakMap
对象
如果target
不是Object
,则进行提示,并返回target
。
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
isObject
:
export const isObject = (val: unknown): val is Record =>
val !== null && typeof val === 'object'
如果target
已经是个proxy
,直接返回target
。reactive(readonly(obj))
是个例外。
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
然后尝试从proxyMap
中获取缓存的proxy
对象,如果存在的话,直接返回proxyMap
中对应的proxy
。否则创建proxy
。
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
为什么要缓存代理对象?
这里缓存对象的存在意义是,一方面避免对同一个对象进行多次代理造成的资源浪费,另一方面可以保证相同对象被代理多次后,代理对象保持一致。例如下面这里例子:
const obj = {}
const objReactive = reactive([obj])
console.log(objReactive.includes(objReactive[0]))
如果没有proxyMap
这个缓存对象,在includes
中由于会访问到数组索引,所以会创建一个obj
的响应式对象,而在includes
的参数中,又访问了依次objReactive
的0索引,所以又会创建个新的obj
代理对象。两次创建的代理对象由于地址不一致,造成objReactive.includes(objReactive[0])
输出为false
。而有了这个缓存对象,当第二次要创建代理对象时,会直接从缓存中获取,这样就保证了相同对象的代理对象地址一致性的问题。
并不是任何对象都可以被proxy
所代理。这里会通过getTargetType
方法来进行判断。
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
getTargetType
:
function getTargetType(value: Target) {
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
? TargetType.INVALID
: targetTypeMap(toRawType(value))
}
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
getTargetType
有三种可能的返回结果
-
TargetType.INVALID
:代表target
不能被代理 -
TargetType.COMMON
:代表target
是Array
或Object
-
TargetType.COLLECTION
:代表target
是Map
、Set
、WeakMap
、WeakSet
中的一种
target
不能被代理的情况有三种:
- 显示声明对象不可被代理(通过向对象添加
__v_skip: true
属性)或使用markRaw
标记的对象 - 对象为不可扩展对象:如通过
Object.freeze
、Object.seal
、Object.preventExtensions
的对象 - 除了
Object
、Array
、Map
、Set
、WeakMap
、WeakSet
之外的其他类型的对象,如Date
、RegExp
、Promise
等
如果targetType !== TargetType.INVALID
,那么则可以进行target
的代理操作了。
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
当new Proxy(target, handler)
时,这里的handler
有两种:一种是针对Object
、Array
的baseHandlers
,一种是针对集合(Set
、Map
、WeakMap
、WeakSet
)的collectionHandlers
。
为什么这里要分两种handler呢?
首先,我们要知道在handler
中我们要进行依赖的收集和依赖的触发。那么什么情况进行依赖收集和触发依赖呢?当我们对代理对象执行读取操作应该收集对应依赖,而当我们对代理对象执行修改操作时应该触发依赖。
那么什么样的操作被称为读取操作和修改操作呢?
读取操作 | 修改操作 | |
---|---|---|
Object | obj.a 、for...in... 、key in obj |
obj.a=1 、delete obj.a |
Array | for...of... 、for...in... 、arr[index] 、arr.length 、arr.indexOf/lastIndexOf/includes(item) 、arr.some/every/forEach 等 |
arr[0]=1 、arr.length=0 、arr.pop/push/unshift/shift 、arr.splice/fill/sort 等 |
集合 | map/set.size 、map.get(key) 、map/set.has(key) 、map/set.forEach 、map.keys/values() 等 |
set.add(value) 、map.add(key, value) 、set/map.clear() 、set/map.delete(key) |
对于Object
、Array
、集合这几种数据类型,如果使用proxy
捕获它们的读取或修改操作,其实是不一样的。比如捕获修改操作进行依赖触发时,Object
可以直接通过set
(或deleteProperty
)捕获器,而Array
是可以通过pop
、push
等方法进行修改数组的,所以需要捕获它的get
操作进行单独处理,同样对于集合来说,也需要通过捕获get
方法来处理修改操作。
接下来看下创建reactive
所需要的两个handler
:mutableHandlers
(Object
与Array
的handler
)、mutableCollectionHandlers
(集合的handler
)。
mutableHandlers
export const mutableHandlers: ProxyHandler
对于Object
和Array
,设置了5个捕获器,分别为:get
、set
、deleteProperty
、has
、ownKeys
。
get捕获器
get
捕获器为属性读取操作的捕获器,它可以捕获obj.pro
、array[index]
、array.indexOf()
、arr.length
、Reflect.get()
、Object.create(obj).foo
(访问继承者的属性)等操作。
const get = /*#__PURE__*/ createGetter()
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 (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
const targetIsArray = isArray(target)
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
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)) {
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
get
捕获器通过一个createGetter
函数创建。createGetter
接收两个参数:isReadonly
是否为只读的响应式数据、shallow
是否是浅层响应式数据。
在get
捕获器中,会先处理几个特殊的key
:
-
ReactiveFlags.IS_REACTIVE
:是不是reactive
-
ReactiveFlags.IS_READONLY
:是不是只读的 -
ReactiveFlags.IS_SHALLOW
:是不是浅层响应式 -
ReactiveFlags.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
}
在获取原始值,有个额外的条件:receiver全等于target的代理对象。为什么要有这个额外条件呢?
这样做是为了避免从原型链上获取不属于自己的原始对象。来看下面一个例子:
const parent = { p:1 }
const parentReactive = reactive(parent)
const child = Object.create(parentReactive)
console.log(toRaw(parentReactive) === parent) // true
console.log(toRaw(child) === parent) // false
声明一个变量parent
并将parent
使用proxy
代理,然后使用Object.create
创建一个对象并将原型指向parent
的代理对象parentReactive
。
这时parentReactive
的原始对象还是parent
,这是毫无疑问的。
如果尝试获取child
的原始对象,因为child
本身是不存在ReactiveFlags.RAW
属性的,所以会沿着原型链向上找,找到parentReactive
时,被parentReactive
的get
拦截器捕获(此时target
是parent
、receiver
是child
),如果没有这条额判断,那么会直接返回target
,也就是parent
,此时意味着child
的原始对象是parent
,这显然是不合理的。恰恰就是这个额外条件排除了这种情况。
然后检查target
是不是数组,如果是数组,需要对一些方法(针对includes
、indexOf
、lastIndexOf
、push
、pop
、shift
、unshift
、splice
)进行特殊处理。
const targetIsArray = isArray(target)
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
通过判断key
是不是arrayInstrumentations
自身包含的属性,处理特殊的数组方法。arrayInstrumentations
是使用createArrayInstrumentations
创建的一个对象,该对象属性包含要特殊处理的数组方法:includes
、indexOf
、lastIndexOf
、push
、pop
、shift
、unshift
、splice
。
为什么要针对这些方法进行特殊处理?
为了弄明白这个问题,我们声明了一个简单的myReactive
,它可以深度创建proxy
。
const obj = {}
function myReactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
if (typeof res === 'object' && res !== null) {
return myReactive(obj)
}
return res
}
})
}
const arr = myReactive([obj])
console.log(arr.includes(obj))
console.log(arr.indexOf(obj))
console.log(arr.lastIndexOf(obj))
当代码执行后,三个打印均为false
,但按照reactive
的逻辑,这三个打印应该打印true
。为什么会出现这个问题呢?当调用includes
、indexOf
、lastIndexOf
这些方法时,会遍历arr
,遍历arr
的过程取到的是reactive
对象,如果拿这个reactive
对象和obj
原始对象比较,肯定找不到,所以需要重写这三个方法。
push
、pop
、shift
、unshift
、splice
这些方法为什么要特殊处理呢?仔细看这几个方法的执行,都会改变数组的长度。以push
为例,我们查看ECMAScript对push
的执行流程说明:
在第二步中会读取数组的length
属性,在第六步会设置length
属性。我们知道在属性的读取过程中会进行依赖的收集,在属性的修改过程中会触发依赖(执行effect.run
)。如果按照这样的逻辑会发生什么问题呢?我们还是以一个例子说明:
const arr = reactive([])
effect(() => {
arr.push(1)
})
当向arr
中进行push
操作,首先读取到arr.length
,将length
对应的依赖effect
收集起来,由于push
操作会设置length
,所以在设置length
的过程中会触发length
的依赖,执行effect.run()
,而在effect.run()
中会执行this.fn()
,又会调用arr.push
操作,这样就会造成一个死循环。
为了解决这两个问题,需要重写这几个方法。
arrayInstrumentations
:
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
function createArrayInstrumentations() {
const instrumentations: Record = {}
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {
// 每个索引都需要进行收集依赖
track(arr, TrackOpTypes.GET, i + '')
}
// 在原始对象上调用方法
const res = arr[key](...args)
// 如果没有找到,可能参数中有响应对象,将参数转为原始对象,再调用方法
if (res === -1 || res === false) {
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// 暂停依赖收集
// 因为push等操作是修改数组的,所以在push过程中不进行依赖的收集是合理的,只要它能够触发依赖就可以
pauseTracking()
const res = (toRaw(this) as any)[key].apply(this, args)
resetTracking()
return res
}
})
return instrumentations
}
回到get
捕获器中,处理玩数组的几个特殊方法后,会使用Reflect.get
获取结果res
。如果res
是symbol
类型,并且key
是Symbol
内置的值,直接返回res
;如果res
不是symbol
类型,且key
不再__proto__
(避免对原型进行依赖追踪)、__v_isRef
、__isVue
中。
const res = Reflect.get(target, key, receiver)
// builtInSymbols: new Set(Object.getOwnPropertyNames(Symbol).map(key => Symbol[key]).filter(val => typeof val === 'symbol'))
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
如果不是只读响应式,就可以调用track
进行依赖的收集。
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
为什么非只读情况才收集依赖?
因为对于只读的响应式数据,是无法对其进行修改的,所以收集它的依赖时没有用的,只会造成资源的浪费。
如果是浅层响应式,返回res
。
if (shallow) {
return res
}
如果res
是ref
,target
不是数组的情况下,会自动解包。
if (isRef(res)) {
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
// 如果target不是数组或key不是整数,自动解包
return shouldUnwrap ? res.value : res
}
如果res
是Object
,进行深层响应式处理。从这里就能看出,Proxy
是懒惰式的创建响应式对象,只有访问对应的key
,才会继续创建响应式对象,否则不用创建。
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
最后,返回res
return res
set捕获器
set
捕获器可以捕获obj.str=''
、arr[0]=1
、arr.length=2
、Reflect.set()
、Object.create(obj).foo = 'foo'
(修改继承者的属性)操作。
const set = /*#__PURE__*/ createSetter()
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)) {
return false
}
if (!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
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
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
}
}
set
捕获器通过一个createSetter
函数创建。createSetter
接收一个shallow
参数,返回一个function
。
set
拦截器中首先获取旧值。如果旧值是只读的ref
类型,而新的值不是ref
,则返回false
,不允许修改。
let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
// 如果不是浅层响应式并且新的值不是readonly
if (!shallow && !isReadonly(value)) {
// 新值不是浅层响应式,新旧值取其对应的原始值
if (!isShallow(value)) {
value = toRaw(value)
oldValue = toRaw(oldValue)
}
// 如果target不是数组并且旧值是ref类型,新值不是ref类型,直接修改oldValue.value为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 obj1 = {}
const obj2 = { a: obj1 }
const obj2Reactive = reactive(obj2)
obj2Reactive.a = reactive(obj1)
console.log(obj2.a === obj1) // true
如果我们不对value
取原始值,在修改obj2Reactive
的a
属性时,会将响应式对象添加到obj2
中,如此原始数据obj2
中会被混入响应式数据,原始数据就被污染了,为了避免这种情况,就需要取value
的原始值,将value
的原始值添加到obj2
中。
那为什么对oldValue
取原始值,因为在后续修改操作触发依赖前需要进行新旧值的比较时,而在比较时,我们不可能拿响应式数据与原始数据进行比较,我们需要拿新值和旧值的原始数据进行比较,只有新值与旧值的原始数据不同,才会触发依赖。
接下来就是调用Reflect.set
进行赋值。
// key是不是target本身的属性
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
然后触发依赖。
// 对于处在原型链上的target不触发依赖
if (target === toRaw(receiver)) {
// 触发依赖,根据hadKey值决定是新增属性还是修改属性
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) // 如果是修改操作,比较新旧值
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
// 返回result
return result
deleteProperty捕获器
deleteProperty
捕获器用来捕获delete obj.str
、Reflect.deletedeleteProperty
操作。
function deleteProperty(target: object, key: string | symbol): boolean {
// key是否是target自身的属性
const hadKey = hasOwn(target, key)
// 旧值
const oldValue = (target as any)[key]
// 调用Reflect.deleteProperty从target上删除属性
const result = Reflect.deleteProperty(target, key)
// 如果删除成功并且target自身有key,则触发依赖
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
// 返回result
return result
}
has捕获器
has
捕获器可以捕获for...in...
、key in obj
、Reflect.has()
操作。
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
// key不是symbol类型或不是symbol的内置属性,进行依赖收集
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key)
}
return result
}
ownKeys捕获器
ownKeys
捕获器可以捕获Object.keys()
、Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
、Reflect.ownKeys()
操作
function ownKeys(target: object): (string | symbol)[] {
// 如果target是数组,收集length的依赖
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
其他reactive
除了reactive
,readonly
、shallowReadonly
、shallowReactive
均是通过createReactiveObject
创建的。不同是传递的参数不同。
export function readonly(
target: T
): DeepReadonly> {
return createReactiveObject(
target,
true,
readonlyHandlers,
readonlyCollectionHandlers,
readonlyMap
)
}
export function shallowReadonly(target: T): Readonly {
return createReactiveObject(
target,
true,
shallowReadonlyHandlers,
shallowReadonlyCollectionHandlers,
shallowReadonlyMap
)
}
export function shallowReactive(
target: T
): ShallowReactive {
return createReactiveObject(
target,
false,
shallowReactiveHandlers,
shallowCollectionHandlers,
shallowReactiveMap
)
}
这里主要看一下readonlyHandlers
的实现。
export const readonlyHandlers: ProxyHandler
因为被readonly
处理的数据不会被修改,所以所有的修改操作都不会被允许,修改操作不会进行意味着也就不会进行依赖的触发,对应地也就不需要进行依赖的收集,所以ownKeys
、has
也就没必要拦截了。
关于集合的处理将在后面文章继续分析。