【vue3源码】七、reactive——Object的响应式实现
【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
方法来处理修改操作。【【vue3源码】七、reactive——Object的响应式实现】接下来看下创建
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 = https://www.it610.com/article/(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 = https://www.it610.com/article/(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 = https://www.it610.com/article/(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
也就没必要拦截了。关于集合的处理将在后面文章继续分析。
推荐阅读
- 笔记|MySql数据库修改密码【详细教程】
- 【JVM知识总结-6】类的文件结构
- 【工程源码】基于FPGA的XPT2046触摸控制器设计
- FPGA|【FPGA】UART串口通信
- FPGA自学|FPGA的I2C的原理及应用(含有源码)
- FPGA|【FPGA】UART串口通信---基于FIFO
- 企业级服务|【产业互联网周报】芯片圈刮起反腐风;快手正式进军toB;三部门:推动绿色低碳技术重大突破
- 10.分说
- 创投日报|钛媒体Pro创投日报:8月8日收录投融资项目10起
- 【微体系】多端全栈项目实战(商业级代驾全流程落地内置文档资料)