一、reactive对象简要介绍
reactive对象是Vue3为最常用的响应式对象之一。其通过代理Proxy实现,是对象类型的一种响应式封装。
二、不同reactive对象的区别
-
reactive
reactive对象是深层响应式对象,即嵌套的对象也会被转换为响应式的代理对象。示例如下:
const proxy = reactive({ out:{ nest:{ a:10 } } }) const nest = proxy.out.nest // nest为Proxy对象 -
shallowReactive
shallowReactive对象是浅层响应式对象,只有访问外层属性才具有响应式,即访问内层属性,不会返回响应式的代理。示例如下:
const proxy = shallowReactive({ out:{ nest:{ a:10 } } }) const out = proxy.out // 非Proxy对象这是因为proxy对象是响应式的,所以可以感知到out属性的变更,但无法感知到nest属性的变更,因为out对象不是代理对象。
-
readonlyReactive
readonlyReactive返回一个对象的只读代理,即无法进行写操作
-
shallowReadonlyReactive
shallowReadonlyReactive返回一个浅层只读代理,即针对外层属性只读,内层属性不限制。示例如下:
const proxy = shallowReadonly({ a:10, b:{ c:20 } }) proxy.a = 20 //error proxy.b.c = 30实际是由于浅响应,深层属性不会转换为代理,因此不会产生拦截。
三、创建对象核心源码解析
下面针对源码进行解析
-
源码如下:
function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any> ) { // reactive只作用于对象 if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target } // 如果目标已经是代理对象,则返回。但如果目标是响应式代理,且新创建的代理是只读代理, // 则需要创建一个新的只读代理(消除原本响应式代理的响应式特性) if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) { return target } // 从代理map中获取 const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } // 判断目标对象是否合法, // 有些对象被打上了标记,不能转换为代理,有些对象是不可扩展的,这些对象不是合法对象 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 }
创建流程比较简洁,代码也有较为详细的注释,不在阐述。
四、读写拦截核心源码解析
1. baseHandler针对普通对象的拦截处理
(1). get
// 数组的封装方法
function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {}
;(['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[]) {
// 针对会更改数据数组长度的方法,暂停依赖收集,
// 因为这些方法会"隐式"的访问和修改length属性
// 不暂停收集可能会导致无限递归
pauseTracking()
const res = (toRaw(this) as any)[key].apply(this, args)
resetTracking()
return res
}
})
return instrumentations
}
// 拦截的核心代码
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 根据特殊key获取特定值 1.是否响应式 2.是否只读 3.是否浅代理 4.被代理对象(原始值)
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) {
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
// 调用数组的封装方法,由于访问数组元素时,需要进行依赖收集,
// 因此针对数组的一些方法进行了二次封装
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
// 获取对应值
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对象,则直接返回。
// 访问其他ref对象需要进行解包,返回值
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// 如果返回值是对象,则需要转换为代理返回,并且继承当前代理对象是否响应式的特性
// 比如当前对象是只读的,那么其嵌套对象的代理也是只读的
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
get方法的代码相对而言还是比较简单。流程图如下:
(2). set
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 获取变更前数据
let oldValue = (target as any)[key]
// 如果前数据是只读且是ref对象,但新数据不是ref对象,则不能更改
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)) {
// 前对象不是数组,且前数据是ref对象,新数据不是ref对象,则直接更改value属性值
oldValue.value = value
return true
}
} else {
// 浅代理下,对象被设置成原样,不论是否响应式
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
// 变更值
const result = Reflect.set(target, key, value, receiver)
// 正常情况下,target就是代理对象receiver的原始对象,这个判断是为了防止多重代理情况下的修改
// 比如target是a,然后有proxyA代理对象a,有proxyB代理proxyA对象,此时通过proxyB修改数据则会在某一阶段存在判断不相等
if (target === toRaw(receiver)) {
if (!hadKey) {
// 如果不存在key,则触发新增类型的依赖更新
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 如果存在key,则触发修改类型的依赖更新
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
set方法的流程相对于get方法,要简洁许多,这儿不再提供流程图。简单来说呢,分为以下几步:
- 判断是否可以修改
- 针对是否浅代理,分别处理
- 在浅代理中,针对ref对象,判断是否只用修改其value值即可
- 根据之前是否存在对应键来触发相应的依赖更新类型
(3). 其他拦截方法
除开最为主要的get,set方法,其实还存在其余几个方法用于依赖收集以及依赖更新,但都不是主要方法,下面简要贴上源码及注释。
// delete关键字
function deleteProperty(target: object, key: string | symbol): boolean {
// 是否存在对应key值
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
// 是否删除成功
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
// 如果存在key值且删除成功,触发delete类型依赖更新
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
// in关键字
function has(target: object, key: string | symbol): boolean {
// 判断是否存在对应key
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
// 如果不是一些内置key值以及特殊symbol类型的key值,则触发has类型依赖收集
track(target, TrackOpTypes.HAS, key)
}
return result
}
// Object.keys
function ownKeys(target: object): (string | symbol)[] {
// 直接触发一个iterate类型的依赖收集,如果数组,则键值说length,如果是对象,则键值是特殊键 ITERATE_KEY
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
2. collectionHandler 针对集合类型的拦截处理
集合类型是一些内置类型,主要是针对以下四类:
- Set
- Map
- WeakSet
- WeakMap
使用集合时,一般都是使用其封装好的方法,类似于使用数组的方法.因此,主要针对这些方法的一个重写做一个简要的解析。
1. get
// get方法只存在于Map和WeakMap中
function get(
target: MapTypes,
key: unknown,
isReadonly = false,
isShallow = false
) {
// 获取map对象的原始值
target = (target as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
// 获取原始key值,传入的key可以是个代理对象
const rawKey = toRaw(key)
if (!isReadonly) {
// 非只读需要依赖收集,原始key以及代理key都收集
if (key !== rawKey) {
track(rawTarget, TrackOpTypes.GET, key)
}
track(rawTarget, TrackOpTypes.GET, rawKey)
}
const { has } = getProto(rawTarget)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
// 判断是否存在相应键值,如果存在则获取对应值,并将其根据map的特性转换为相应的代理对象
// map浅代理-不做转换
// map只读-转换只读代理
// map响应式-转换响应式代理
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
} else if (target !== rawTarget) {
// #3602 readonly(reactive(Map))
// ensure that the nested reactive `Map` can do tracking for itself
target.get(key)
}
}
get方法是Map和WeakMap集合所拥有的方法。在通过get方法获取对应值时,先收集相关依赖key,然后将对应值进行相应的响应式转换。代码中的key可能是一个代理对象,所以获取了其原始值rawKey,2个key值都进行判断是为了兼容传入代理对象的情况。
2. has
// 集合类型都存在的方法
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
const target = (this as any)[ReactiveFlags.RAW]
// 获取原始集合对象以及原始键值
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
// 非只读进需要收集依赖
if (!isReadonly) {
if (key !== rawKey) {
track(rawTarget, TrackOpTypes.HAS, key)
}
track(rawTarget, TrackOpTypes.HAS, rawKey)
}
// 调用集合原始has方法返回结果
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
}
3. 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)
}
4. add
// 添加元素,Set,WeakSet类型的方法
function add(this: SetTypes, value: unknown) {
// 获取原始值
value = toRaw(value)
// 获取原始几乎
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
if (!hadKey) {
// 集合中不存在对应的值,则添加,并且触发依赖更新
target.add(value)
trigger(target, TriggerOpTypes.ADD, value, value)
}
return this
}
5. set
// 设置键值对,Map和WeakMap类型的方法
function set(this: MapTypes, key: unknown, value: unknown) {
value = toRaw(value)
const target = toRaw(this)
const { has, get } = getProto(target)
// 判断是否存在对应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)
// 根据是否存在对应键值触发添加和修改的依赖更新
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 修改需要判断前后值是否变更
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return this
}
6. delete
// 集合都拥有的方法 delete触发
function deleteEntry(this: CollectionTypes, key: unknown) {
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
// 获取是否存在key
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
// forward the operation before queueing reactions
// 直接删除,不论是否存在,因为不存在也无影响
const result = target.delete(key)
if (hadKey) {
// 只有存在key,才触发delete依赖更新
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
7. 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()
if (hadItems) {
// 如果集合之前存在元素,则触发clear依赖更新
trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
}
return result
}
8. forEach
// 集合的遍历方法 forEach
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)
// 根据集合特性获取集合元素的包装方法 1.不处理 2.只读 3.响应式
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
// 非只读进行依赖收集
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
// 返回包装后的遍历值
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
}
9. 疑惑点阐述
在上述所有集合,都用到了一个方法toRaw,这是获取原始值的方法,本质上是递归获取ReactiveFlags.RAW这个属性的值。方法实现如下:
export function toRaw<T>(observed: T): T {
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
return raw ? toRaw(raw) : observed
}
这个方法的实际作用就是获取一个代理对象的原始值,比如存在一个对象a,代理对象proxyA是a的代理,那么toRaw(proxyA)最终能获取到对象a,并且当存在多级代理时,也可以获取到最原始的那个被代理对象,因为toRaw是个递归调用。直接访问ReactiveFlags.RAW只会获取当前代理的原始值,如果存在多级代理,则获取到的值还是代理对象。那么为什么需要获取原始值?
本质上讲,使用代理只是为了新增一些自定义逻辑,主要是依赖的收集和更新,原本的针对集合的读写逻辑不应该被修改。因此不管读写,最终操作的都是原始数据以及原始集合,使用toRaw转换,是为了支持代理key的传入。这个模型如下图:
从上图可知,不管读写,本质上都是先转换为原始数据,在进行操作。那么为什么需要转换?因为在Vue3中,我们绝大多数时候使用的都是响应式对象,这种设计方法就是为了不论传入响应式对象还是原始对象,都能正确的存储,简单测试代码如下:
const map = reactive(new Map())
let obj = {}
let obj1 = {}
map.set(reactive(obj),1)
map.set(obj1,2)
expect(map.get(obj)).toBe(1)
expect(map.has(obj)).toBe(true)
expect(map.get(reactive(obj1))).toBe(2)
expect(map.has(reactive(obj))).toBe(true)
在上述测试代码中,设置的key值和获取的key值,分别是代理对象和非代理对象,但这完全不影响正常的读写逻辑,并且还有正常的依赖收集。
五、总结
Vue3响应式对象reactive的分析就到这儿了,本质上就是一个代理对象,在拦截操作的过程中,加入了依赖更新和依赖收集的流程,后续博客会介绍ref对象的代码解析。