Vue3 reactivity 源码解读 —— 第一部分
2024-09-29 星期日# 什么是响应式?
这个借鉴了观察者模式的思想,我们可以把数据看作一个依赖,副作用函数(渲染函数或者其他的函数)看作一个订阅者。当数据发生变化时,通知订阅者。
# 响应式对象是如何被创建的?
Important
响应式对象的本质是一个代理对象。
我们一般都是使用 reactive() 创建的,使用它创建的响应式对象都是深层次的。
tsimport { reactive } from '@vue/reactivity' const obj = reactive({ a: { b: 1 } }) console.log(obj.a.b) // 输出 1
其实还有其他的响应式变体,比如:shallowReactive、readonly、shallowReadonly。
创建一个reactive.ts文件,我们可以尝试实现一个reactive函数(返回类型我们先忽略)。它会返回一个代理对象。
Note
为什么要使用 Proxy API?
答:因为由于代理对象可以拦截对属性的操作,我们就可以知道用户使用了哪些属性,当属性重新设置的时候就可以更新视图了。
Note
为什么要使用 Reflect API?
答:Reflect 提供了一种更加一致和可靠的方式来访问对象属性。它是默认的操作行为,并且与常规的属性访问相同,但在某些边缘情况下更可预测。而且对于使用getter时,允许我们指定一个receiver
参数,这个参数决定了getter的this指向。
# reactive 实现方式
reactive()
将对象转换为一个深层响应式对象(即对嵌套对象生效),返回一个代理对象。
tsexport function reactive<T extends object>(target: T) { return new Proxy(target, { get(target, key, receiver) { // 依赖收集... return Reflect.get(target, key, receiver) }, set(target, key, receiver) { // 派发更新... return Reflect.set(target, key, receiver) } // ...其他的处理方法 }) }
考虑到用户可能把一个已经代理的对象传入,我们需要做一个缓存,避免重复代理,这里可以使用WeakMap来实现。
Note
为什么选择WeakMap呢?
答:因为它的key必须是一个引用类型(一个对象或者是非全局的Symbol),因为它们是可垃圾回收的,WeakMap的键是创建它们的弱引用,即如果没有其他地方使用这个key,它就会被回收,而他对应的value也会被回收。这个使用场景非常适合响应式数据,当用户不需要时,就可以进行回收内存了。
tsconst reactiveMap = new WeakMap<object, any>() export function reactive<T extends object>(target: T) { const existingProxy = reactiveMap.get(target) if (existingProxy) { return existingProxy } const proxy = new Proxy(target, { get(target, key, receiver) { // 依赖收集... return Reflect.get(target, key, receiver) }, set(target, key, receiver) { // 派发更新... return Reflect.set(target, key, receiver) } // ...其他的处理方法 }) reactiveMap.set(target, proxy) return proxy }
这样就实现一个简单的 reactive 函数了。现在可以尝试实现其他的reactive变体了,比如:shallowReactive、readonly、shallowReadonly。
reactive()源码实现:
ts// reactive.ts export function reactive<T extends object>(target: T): Reactive<T> export function reactive(target: object) { if (isReadonly(target)) { return target } return createReactiveObject( target, false, // 是否只读 mutableHandlers, // 可变基本处理器 mutableCollectionHandlers, // 可变集合处理器 reactiveMap, ) }
# readonly 实现方式
readonly()
可以将一个对象或者响应式对象转变为只读的响应式对象(对嵌套对象生效),它会返回一个代理对象。
tsconst readonlyMap = new WeakMap<object, any>() export function readonly<T extends object>(target: T) { const existingProxy = readonlyMap.get(target) if (existingProxy) { return existingProxy } const proxy = new Proxy(target, { get(target, key, receiver) { // 依赖收集... return Reflect.get(target, key, receiver) }, set(target, key, receiver) { console.warn(`不能修改只读对象的属性${key}`) return true }, // ...其他的处理方法 }) readonlyMap.set(target, proxy) return proxy }
大家有没有发现,readonly和reactive实现方式的逻辑相同,只是代理的处理器不同和存放代理对象的map不同。这个时候就可以使用一个工厂函数来创建代理对象了,其实源码中也是这样实现的。
readonly()源码实现:
ts// reactive.ts export function readonly<T extends object>( target: T, ): DeepReadonly<UnwrapNestedRefs<T>> { return createReactiveObject( target, true, readonlyHandlers, readonlyCollectionHandlers, readonlyMap, ) }
# createReactiveObject 实现
这个工厂函数根据传入的handlers
和proxyMap
来创建不同的响应式对象。
源码中还针对集合类型做了专门处理,这个之后再讲。
tsimport { isObject } from '@vue/shared' export function createReactiveObject<T extends object>( target: T, handlers: ProxyHandler<T>, proxyMap: WeakMap<T, any>, ) { if (!isObject(target)) { return target } const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } const proxy = new Proxy(target, handlers) proxyMap.set(target, proxy) return proxy }
createReactiveObject()源码实现:
tsimport { isObject, toRawType } from '@vue/shared' enum TargetType { INVALID, // 无效的 COMMON, // 普通对象 COLLECTION // 集合 } // 根据目标对象的类型字符串返回相应的 TargetType 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 } } // 获取目标对象的类型 function getTargetType(value: Target) { // 如果目标跳过响应式代理或者目标不可扩展,那么直接返回无效的目标类型,否则将获取对应的目标类型 return value[ReactiveFlags.SKIP] || !Object.isExtensible(value) ? TargetType.INVALID : targetTypeMap(toRawType(value)) } function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any>, ) { if (!isObject(target)) { // 在开发模式下打印警告... return target } // 如果目标对象已经是一个响应式对象,返回目标对象 // 例外:在只读的情况下,如果目标对象已经是一个响应式对象,则创建一个只读代理,否则返回目标对象 if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) { return target } // 如果目标对象已经存在一个代理对象,返回代理对象 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 }
现在让我们改造reactive和readonly,让它们使用createReactiveObject工厂函数来创建代理对象。 对于每个响应式对象的处理器,我们将在之后的篇章进行详细讲解,现在请我们忽略它。
tsimport { reactiveHandlers, readonlyHandlers } from './baseHandlers' export function reactive<T extends object>(target: T) { return createReactiveObject( target, reactiveHandlers, reactiveMap ) } export function readonly<T extends object>(target: T) { return createReactiveObject( target, readonlyHandlers, readonlyMap ) }
这样写的话,reactive()
还是有一个问题,那就是如果传入一个readonly的对象时,不应该代理这个对象,而是直接返回这个readonly对象。那么我们需要一个函数来判断这个对象是否是readonly对象。
# 区分响应式对象和只读对象,以及其他的响应式变体
但是有一个问题,我们如何区分响应式对象和只读对象呢?我们可以使用一个唯一的标记来标记响应式对象。
ts// 响应式对象的标记 export enum ReactiveFlags { SKIP = '__v_skip', // 是否跳过响应式转换 IS_REACTIVE = '__v_isReactive', // 是否是响应式对象 IS_READONLY = '__v_isReadonly', // 是否是只读对象 IS_SHALLOW = '__v_isShallow', // 是否只是浅响应式对象 RAW = '__v_raw', // 原始对象 IS_REF = '__v_isRef', // 是否是ref对象 } // 一个响应式对象的接口,通过这个接口可以区分响应式对象和只读对象 export interface Target { [ReactiveFlags.SKIP]?: boolean [ReactiveFlags.IS_REACTIVE]?: boolean [ReactiveFlags.IS_READONLY]?: boolean [ReactiveFlags.IS_SHALLOW]?: boolean [ReactiveFlags.RAW]?: any }
源码中
ReactiveFlags
的定义在 constants.ts 中。
# isReadonly 实现
通过响应式标记,我们就可以判断对象是否为只读的了,只要传入的值存在并且它的响应式标记为只读那么它就是一个readonly对象。
使用!!是强行将值转换为布尔值,因为表达式的结果可能不是一个布尔值,因为短路会返回表达式的最终值。
tsexport function isReadonly(value: unknown): boolean { return !!(value && (value as Target)[ReactiveFlags.IS_READONLY]) }
这样我们就可以通过isReadonly
来判断对象是否是只读对象了。
修改reactive的代码如下:
tsexport function reactive<T extends object>(target: T) { if (isReadonly(target)) { return target } // 省略... }
isReadonly()源码实现:
ts// reactive.ts export function isReadonly(value: unknown): boolean { return !!(value && (value as Target)[ReactiveFlags.IS_READONLY]) }
我们还需要考虑以下边界情况:
例1:重复调用 reactive()
tsconst reactiveObj1 = reactive({ foo: 1 }) const reactiveObj2 = reactive (obj) // 如果是一个响应式对象,应该直接返回它 console.log(reactiveObj1 === reactiveObj2) // true
例2:尝试将 reactive 转换为 readonly 。
tsconst reactiveObj = reactive({ foo: 1 }) const readonlyObj = readonly(reactiveObj) // 如果是一个响应式对象,创建一个新的只读响应式对象 console.log(reactiveObj === readonlyObj) // false
例3:将只读对象传入 readonly。
tsconst reactiveObj = reactive({ foo: 1 }) const readonlyObj = readonly(reactiveObj) const readonlyObj2 = readonly(readonlyObj) // 如果是一个只读对象,返回它 console.log(reactiveObj === readonlyObj2) // true
这样我们需要使用 ReactiveFlags
来判断是否是响应式对象,并且还需要一个标记表示当前创建的是否为只读对象。 修改 createReactiveObject() 的代码如下:
tsexport function createReactiveObject( target: Target, isReadonly: boolean, // 表示创建的是只读对象 handlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any>, ) { // 省略... // 对于为什么能读取到`ReactiveFlags`,将会在handlers的部分中揭晓 // 如果是一个响应式对象(不管是深层还是浅层),直接返回它 // 例外情况: 调用 readlony() 时传入的是一个响应式对象,则创建一个只读代理,否则返回目标对象 if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) { return target } // 省略... }
# shallow 的定义
什么是shallow(浅层),即只代理顶层属性,而不代理深层属性。
tsconst obj = { a: { b: 1 }, c: 2 } // 属于浅层 obj.a obj.c // 属于深层 obj.a.b
# shallowReactive 实现
这个函数创建一个浅层响应式对象,即只有顶层属性具备响应式能力,而深层属性不具备响应式能力。
使用场景:
- 性能优化:当只需要顶层属性具备响应式能力时,可以使用shallowReactive来创建浅层响应式对象,这样可以提高性能。
- 部分状态管理:可以使用浅层响应式对象的深层属性创建局部状态,这样可以避免不必要的性能开销。
tsconst shallowReactiveMap = new WeakMap<Target, any>() export function shallowReactive<T extends object>(target: T) { return createReactiveObject( target, false, // 是否只读 shallowReactiveHandlers, // 浅层处理器 shallowCollectionHandlers, // 浅层集合处理器 shallowReactiveMap, // 浅层响应式Map ) }
shallowReactive()源码实现:
ts// reactive.ts export function shallowReactive<T extends object>( target: T, ): ShallowReactive<T> { return createReactiveObject( target, false, shallowReactiveHandlers, shallowCollectionHandlers, shallowReactiveMap, ) }
# shallowReadonly 实现
这个函数创建一个浅层只读对象,即只有顶层属性是只读的,而深层属性是可以修改的。
tsexport function shallowReadonly<T extends object>(target: T) { return createReactiveObject( target, true, // 是否只读 shallowReadonlyHandlers, // 浅层只读处理器 shallowReadonlyCollectionHandlers, // 浅层只读集合处理器 shallowReadonlyMap, // 浅层只读Map ) }
shallowReadonly()源码实现:
ts// reactive.ts export function shallowReadonly<T extends object>(target: T): Readonly<T> { return createReactiveObject( target, true, shallowReadonlyHandlers, shallowReadonlyCollectionHandlers, shallowReadonlyMap, ) }
# 其他的工具函数
让我们来实现剩下的工具函数。
# isReactive 实现
这个函数判断一个值是否为响应式对象,只要判断是否存在ReactiveFlags.IS_REACTIVE
标记即可。
tsexport function isReactive(value: unknown): boolean { return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE]) }
这样可能会有一些问题,比如:如果对象是一个只读对象,那么它是不是响应式对象呢?这个得分情况讨论一下。
tsconst original = { foo: 1 } // case 1 const reactiveObj = reactive(original) isReactive(reactiveObj) // true,很明显,这具备响应式功能 // case 2 const readonlyObj1 = readonly(original) isReactive(readonlyObj1) // 结果为false,预计为false,因为这不具备响应式功能,因为它是接收的不是一个代理,而是一个原始对象 // case 3 const readonlyObj2 = readonly(reactiveObj) isReactive(readonlyObj2) // 结果为false,预计为true,但是这具备响应式功能,因为它接收的是一个代理,当代理的响应式对象改变时,它也会改变 // case 4 const readonlyObj3 = readonly(readonlyObj2) isReactive(readonlyObj3) // 结果为false,预计为true,这个应该也具备响应式,因为调用readonly()传入一个readonly对象,会之间返回它,这样就和case3是一样的
这样来看只要代理链上层中只要有一个响应式对象,那么它就是响应式对象,那么如何解决这个问题呢?
源码中还考虑了当传入的值是一个ref对象时,应该如何处理。
isReactive()源码实现:
ts// reactive.ts /** * @example * ```js * isReactive(reactive({})) // => true * isReactive(readonly(reactive({}))) // => true * isReactive(ref({}).value) // => true * isReactive(readonly(ref({})).value) // => true * isReactive(ref(true)) // => false * isReactive(shallowRef({}).value) // => false * isReactive(shallowReactive({})) // => true * ``` */ export function isReactive(value: unknown): boolean { // 如果是只读对象就获取它的原始对象,递归判断原始对象是否为响应式对象,只要代理链中有一个响应式对象,那么它就是响应式对象 if (isReadonly(value)) { return isReactive((value as Target)[ReactiveFlags.RAW]) } return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE]) }
# isShallow 实现
这个函数判断一个值是否为浅响应式对象,只要判断是否存在ReactiveFlags.IS_SHALLOW
标记即可。
通过以下例子即可:
tsconst shallowObj = shallowReactive({ foo: 1 }) const shallowReadonlyObj = shallowReadonly({ foo: 1 }) isShallow(shallowObj) // true isShallow(shallowReadonlyObj) // true
tsexport function isShallow(value: unknown): boolean { return !!(value && (value as Target)[ReactiveFlags.IS_SHALLOW]) }
isShallow()
源码同上。
# isProxy 实现
这个函数判断一个值是否为代理对象,只要判断ReactiveFlags.RAW
对象是否存在即可。
tsexport function isProxy(value: unknown): boolean { return !!(value && (value as Target)[ReactiveFlags.RAW]) }
isProxy()源码实现:
ts// reactive.ts export function isProxy(value: any): boolean { return value ? !!(value as Target)[ReactiveFlags.RAW] : false }
# toRaw 实现
这个函数返回响应式对象(如果是多层代理,那么返回的是最原始的响应式对象)的原始对象,如果传入的是一个原始对象,那么直接返回它。
tsexport function toRaw<T>(observed: T): T { const raw = observed && (observed as Target)[ReactiveFlags.RAW] // 如果原始对象存在,那么递归获取最原始的响应式对象 return raw ? toRaw(raw) : observed }
toRaw()
源码同上。
# markRaw 实现
这个函数将标记这个对象,即不会被转换为响应式对象,并返回它。
使用方式:
tsconst markRawObj = markRaw({ foo: 1 }) // 标记为原始对象,这个对象不能转换为响应式对象 const reactiveObj = reactive(markRawObj) // 返回这个原始对象 console.log(reactiveObj === markRawObj) // true
Note
为什么不使用Reflect.set呢? 因为Reflect.set会被代理拦截。
tsexport function markRaw<T extends object>(value: T): T { // 如果对象可以扩展,则添加ReactiveFlags.SKIP标记,跳过响应式转换。 // Reflect.defineProperty和Object.defineProperty的区别就是它返回是布尔值 if (Object.isExtensible(value)) { Reflect.defineProperty(value, ReactiveFlags.SKIP, { value: true, writable: false, enumerable: false, configurable: false }) } return value }
markRaw
ts// reactive.ts import { def } from '@vue/shared' export function markRaw<T extends object>(value: T): Raw<T> { if (Object.isExtensible(value)) { def(value, ReactiveFlags.SKIP, true) } return value }
# toReactive 实现
这个函数将一个值转换为响应式对象,如果不是对象,那么直接返回它。
tsexport function toReactive<T>(value: T): T { return isObject(value) ? reactive(value) : value }
toReactive()
源码同上。
# toReadonly 实现
这个函数将一个值转换为只读对象,如果不是对象,那么直接返回它。
tsexport function toReadonly<T>(value: T): T { return isObject(value) ? readonly(value) : value }
toReadonly()
源码同上。
Comment / 留言区
暂无留言