前言
在我们请求数据后,有一些数据我们并不会用到,因此我们可以给他处理成非响应式(Non-reactive Object)的,以提高我们的系统的性能。
export default {
data() {
return {
list: [
{
title: 'item1'
msg: 'I am item1',
extData: {
type: 1
}
},
...
]
}
}
}
</script>
1、认识Reactivity Object 基础
在vue3中通过Reactivity Object创建响应式对象,是通过proxy创建原始响应对象和使用reflect对JavaScript代理。 通过vue3的ref,reactive,等api创建出来的对象都是响应式的。但有些数据我们并不需要他是响应式的,故从源码角度解决。
2、源码角度解决非响应式数据
首先,我们可以建立一个简单的认知,那就是对于 Non-reactivity Object 的处理肯定是是发生在创建响应式对象之前,我想这一点也很好理解。在源码中,创建响应式对象的过程则都是由 packages/reactivity/src/reactive.ts 文件中一个名为 createReactiveObject 的函数实现的。
2.1createReactiveObject
vue3创建响应式对象源码
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {}
createReactiveObject 函数总共会接收 5 个参数:
target表示需要创建成响应式对象的原始对象isReadonly表示创建后的响应式对象是要设置为只读baseHandlers表示创建Proxy所需要的基础handler,主要有get、set、deleteProperty、has和ownKeys等collectionHandlers表示集合类型(Map、Set等)所需要的handler,它们会重写add、delete、forEach等原型方法,避免原型方法的调用中访问的是原始对象,导致失去响应的问题发生proxyMap表示已创建的响应式对象和原始对象的WeekMap映射,用于避免重复创建基于某个原始对象的响应式对象
然后,在 createReactiveObject 函数中则会做一系列前置的判断处理,例如判断 target 是否是对象、target 是否已经创建过响应式对象(下面统称为 Proxy 实例)等,接着最后才会创建 Proxy 实例。
那么,显然 Non-reactivity Object 的处理也是发生 createReactiveObject 函数的前置判断处理这个阶段的,其对应的实现会是这样(伪代码):
function createReactiveObject(...) {
// ...
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// ...
}
当targetType等于TargetType.INVALID,我们就直接返回对象。
解析一下getTargetType(target)和TargetType.INVALID是什么
2.2getTargetType 和 targetType
getTargetType 函数的实现:
// core/packages/reactivity/src/reactive.ts
function getTargetType(value: Target) {
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
? TargetType.INVALID
: targetTypeMap(toRawType(value))
}
其中 getTargetType 主要做了这 3 件事:
- 判断
target上存在ReactiveFlags.SKIP属性,它是一个字符串枚举,值为__v_ship,存在则返回TargetType.INVALID - 判断
target是否可扩展Object.isExtensible返回true或false,为true则返回TargetType.INVALID - 在不满足上面 2 者的情况时,返回
targetTypeMap(toRawType(value))
从 1、2 点可以得出,只要你在传入的 target 上设置了 __v_ship 属性、或者使用 Object.preventExtensions、Object.freeze、Object.seal 等方式设置了 target 不可扩展,那么则不会创建 target 对应的响应式对象,即直接返回 TargetType.INVALID(TargetType 是一个数字枚举,后面会介绍到)。
在我们上面的这个例子就是设置 extData:
{
type: 1,
__v_ship: true
}
或者:
Object.freeze({
type: 1
})
那么,在第 1、2 点都不满足的情况下,则会返回 targetTypeMap(toRawType(value)),其中 toRawType 函数则是基于 Object.prototype.toString.call 的封装,它最终会返回具体的数据类型,例如对象则会返回 Object:
// core/packages/shared/src/index.ts
const toRawType = (value: unknown): string => {
// 等于 Object.prototype.toString.call(value).slice(8, -1)
return toTypeString(value).slice(8, -1)
}
然后,接着是 targetTypeMap 函数:
// core/packages/reactivity/src/reactive.ts
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
}
}
可以看到,targetTypeMap 函数实际上是对我们所认识的数据类型做了 3 个分类:
TargetType.COMMON表示对象Object、 数组ArrayTargetType.COLLECTION表示集合类型,Map、Set、WeakMap、WeakSetTargetType.INVALID表示不合法的类型,不是对象、数组、集合
其中,TargetType 对应的枚举实现:
const enum TargetType {
INVALID = 0,
COMMON = 1,
COLLECTION = 2
}
那么,回到我们上面的这个例子,由于 list.extData 在 toRawType 函数中返回的是数组 Array,所以 targetTypeMap 函数返回的类型则会是 TargetType.COMMON(不等于 TargetType.INVALID),也就是最终会为它创建响应式对象。
因此,在这里我们可以得出一个结论,如果我们需要跳过创建响应式对象的过程,则必须让 target 满足 value[ReactiveFlags.SKIP] || !Object.isExtensible(value) 或者命中 targetTypeMap 函数中的 default 逻辑。
结语
通过阅读我们可以避免嵌套对象的创建响应式过程,在某些情况下可以很好的优化性能。