【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是不是只读的,baseHandlersproxy的捕获器,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,直接返回targetreactive(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:代表targetArrayObject
  • TargetType.COLLECTION:代表targetMapSetWeakMapWeakSet中的一种
target不能被代理的情况有三种:
  1. 显示声明对象不可被代理(通过向对象添加__v_skip: true属性)或使用markRaw标记的对象
  2. 对象为不可扩展对象:如通过Object.freezeObject.sealObject.preventExtensions的对象
  3. 除了ObjectArrayMapSetWeakMapWeakSet之外的其他类型的对象,如DateRegExpPromise
如果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有两种:一种是针对ObjectArraybaseHandlers,一种是针对集合(SetMapWeakMapWeakSet)的collectionHandlers
为什么这里要分两种handler呢?
首先,我们要知道在handler中我们要进行依赖的收集和依赖的触发。那么什么情况进行依赖收集和触发依赖呢?当我们对代理对象执行读取操作应该收集对应依赖,而当我们对代理对象执行修改操作时应该触发依赖。
那么什么样的操作被称为读取操作和修改操作呢?
读取操作 修改操作
Object obj.afor...in...key in obj obj.a=1delete obj.a
Array for...of...for...in...arr[index]arr.lengtharr.indexOf/lastIndexOf/includes(item)arr.some/every/forEach arr[0]=1arr.length=0arr.pop/push/unshift/shiftarr.splice/fill/sort
集合 map/set.sizemap.get(key)map/set.has(key)map/set.forEachmap.keys/values() set.add(value)map.add(key, value)set/map.clear()set/map.delete(key)
对于ObjectArray、集合这几种数据类型,如果使用proxy捕获它们的读取或修改操作,其实是不一样的。比如捕获修改操作进行依赖触发时,Object可以直接通过set(或deleteProperty)捕获器,而Array是可以通过poppush等方法进行修改数组的,所以需要捕获它的get操作进行单独处理,同样对于集合来说,也需要通过捕获get方法来处理修改操作。
【【vue3源码】七、reactive——Object的响应式实现】接下来看下创建reactive所需要的两个handlermutableHandlersObjectArrayhandler)、mutableCollectionHandlers(集合的handler)。
mutableHandlers
export const mutableHandlers: ProxyHandler = { get, set, deleteProperty, has, ownKeys }
对于ObjectArray,设置了5个捕获器,分别为:getsetdeletePropertyhasownKeys
get捕获器 get捕获器为属性读取操作的捕获器,它可以捕获obj.proarray[index]array.indexOf()arr.lengthReflect.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时,被parentReactiveget拦截器捕获(此时targetparentreceiverchild),如果没有这条额判断,那么会直接返回target,也就是parent,此时意味着child的原始对象是parent,这显然是不合理的。恰恰就是这个额外条件排除了这种情况。
然后检查target是不是数组,如果是数组,需要对一些方法(针对includesindexOflastIndexOfpushpopshiftunshiftsplice)进行特殊处理。
const targetIsArray = isArray(target)if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) }

通过判断key是不是arrayInstrumentations自身包含的属性,处理特殊的数组方法。arrayInstrumentations是使用createArrayInstrumentations创建的一个对象,该对象属性包含要特殊处理的数组方法:includesindexOflastIndexOfpushpopshiftunshiftsplice
为什么要针对这些方法进行特殊处理?
为了弄明白这个问题,我们声明了一个简单的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。为什么会出现这个问题呢?当调用includesindexOflastIndexOf这些方法时,会遍历arr,遍历arr的过程取到的是reactive对象,如果拿这个reactive对象和obj原始对象比较,肯定找不到,所以需要重写这三个方法。
pushpopshiftunshiftsplice这些方法为什么要特殊处理呢?仔细看这几个方法的执行,都会改变数组的长度。以push为例,我们查看ECMAScript对push的执行流程说明:
【vue3源码】七、reactive——Object的响应式实现
文章图片

在第二步中会读取数组的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。如果ressymbol类型,并且keySymbol内置的值,直接返回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 }

如果resreftarget不是数组的情况下,会自动解包。
if (isRef(res)) { const shouldUnwrap = !targetIsArray || !isIntegerKey(key) // 如果target不是数组或key不是整数,自动解包 return shouldUnwrap ? res.value : res }

如果resObject,进行深层响应式处理。从这里就能看出,Proxy是懒惰式的创建响应式对象,只有访问对应的key,才会继续创建响应式对象,否则不用创建。
if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res) }

最后,返回res
return res

set捕获器 set捕获器可以捕获obj.str=''arr[0]=1arr.length=2Reflect.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取原始值,在修改obj2Reactivea属性时,会将响应式对象添加到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.strReflect.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 objReflect.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 除了reactivereadonlyshallowReadonlyshallowReactive均是通过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 = { get: readonlyGet, set(target, key) { if (__DEV__) { warn( `Set operation on key "${String(key)}" failed: target is readonly.`, target ) } return true }, deleteProperty(target, key) { if (__DEV__) { warn( `Delete operation on key "${String(key)}" failed: target is readonly.`, target ) } return true } }
因为被readonly处理的数据不会被修改,所以所有的修改操作都不会被允许,修改操作不会进行意味着也就不会进行依赖的触发,对应地也就不需要进行依赖的收集,所以ownKeyshas也就没必要拦截了。
关于集合的处理将在后面文章继续分析。

    推荐阅读