前言
在 Vue2 中,有一个老生常谈的话题,如何避免 data
中一个复杂对象(自身或属性对象)被默认被创建为响应式(Non-reactive Object)的过程? 举个例子,有一个 Vue2 的组件的 data
:
<script>
export default {
data() {
return {
list: [
{
title: 'item1'
msg: 'I am item1',
extData: {
type: 1
}
},
...
]
}
}
}
</script>
复制代码
这里我们希望 list.extData
不被创建为响应式的对象,相信很多同学都知道,我们可以通过 Object.defineProperty
设置对象 list.extData
的 configurable
属性为 false
来实现。
而在 Vue2 中,我们可以这么做,但是回到 Vue3,同样的问题又要怎么解决呢? 我想这应该是很多同学此时心中持有的疑问。所以,下面让我们一起来由浅至深地去解开这个问题。
1 认识 Reactivity Object 基础
首先,我们先来看一下 Reactivity Object 响应式对象,它是基于使用 Proxy
创建一个原始对象的代理对象和使用 Reflect
来代理 JavaScript 操作方法,从而完成依赖的收集和派发更新的过程。
然后,我们可以根据需要通过使用 Vue3 提供的 ref
、compute
、reactive
、readonly
等 API 来创建对应的响应式对象。
这里,我们来简单看个例子:
import { reactive } from '@vue/reactivity'
const list = reactive([
{
title: 'item1'
msg: 'I am item1',
extData: {
type: 1
}
}
])
复制代码
可以看到,我们用 reactive
创建了一个响应式数据 list
。并且,在默认情况下 list
中的每一项中的属性值为对象的都会被处理成响应式的,在这个例子就是 extData
,我们可以使用 Vue3 提供的 isReactive
函数来验证一下:
console.log(`extData is reactive: ${isReactive(list[0].extData)}`)
// 输出 true
复制代码
控制台输出:
可以看到 extData
对应的对象确实是被处理成了响应式的。假设,list
是一个很长的数组,并且也不需要 list
中每一项的 extData
属性的对象成为响应式的。那么这个默然创建响应式的对象过程,则会产生我们不期望有的性能上的开销(Overhead)。
既然,是我们不希望的行为,我们就要想办法解决。所以,下面就让我们从源码层面来得出如何解决这个问题。
2 源码中对 Non-reactivity Object 的处理
首先,我们可以建立一个简单的认知,那就是对于 Non-reactivity Object 的处理肯定是是发生在创建响应式对象之前,我想这一点也很好理解。在源码中,创建响应式对象的过程则都是由 packages/reactivity/src/reactive.ts
文件中一个名为 createReactiveObject
的函数实现的。
2.1 createReactiveObject
这里,我们先来看一下 createReactiveObject
函数的签名:
// core/packages/reactivity/reactive.ts
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {}
复制代码
可以看到 createReactiveObject
函数总共会接收 5 个参数,我们分别来认识这 5 个函数形参的意义:
target
表示需要创建成响应式对象的原始对象isReadonly
表示创建后的响应式对象是要设置为只读baseHandlers
表示创建Proxy
所需要的基础handler
,主要有get
、set
、deleteProperty
、has
和ownKeys
等collectionHandlers
表示集合类型(Map
、Set
等)所需要的handler
,它们会重写add
、delete
、forEach
等原型方法,避免原型方法的调用中访问的是原始对象,导致失去响应的问题发生proxyMap
表示已创建的响应式对象和原始对象的WeekMap
映射,用于避免重复创建基于某个原始对象的响应式对象
可能会有同学对
collectionHandlers
函数会感到有一点困惑,由于这属于不是本文这次重点要表达的内容,有兴趣的同学可以自行了解,或者私聊我交流~
然后,在 createReactiveObject
函数中则会做一系列前置的判断处理,例如判断 target
是否是对象、target
是否已经创建过响应式对象(下面统称为 Proxy
实例)等,接着最后才会创建 Proxy
实例。
那么,显然 Non-reactivity Object 的处理也是发生 createReactiveObject
函数的前置判断处理这个阶段的,其对应的实现会是这样(伪代码):
// core/packages/reactivity/src/reactive.ts
function createReactiveObject(...) {
// ...
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// ...
}
复制代码
可以看到,只要使用 getTargetType
函数获取传入的 target
类型 targetType
等于 TargetType.INVALID
的时候,则会直接返回原对象 target
,也就是不会做后续的响应式对象创建的过程。
那么,这个时候我想大家都会有 2 个疑问:
getTargetType
函数做了什么?TargetType.INVALID
表示什么,这个枚举的意义?
下面,让我们分别来一一解开这 2 个疑问。
2.2 getTargetType 和 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
、 数组Array
TargetType.COLLECTION
表示集合类型,Map
、Set
、WeakMap
、WeakSet
TargetType.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
逻辑。
结语
阅读到这里,我想大家都明白了如何在创建一个复杂对象的响应式对象的时候,跳过对象中一些嵌套对象的创建响应式的过程。并且,这个小技巧在某些场景下,不可否认的是一个很好的优化手段,所以提前做好必要的认知也是很重要的。
最后,如果文中存在表达不当或错误的地方,欢迎各位同学提 Issue ~
点赞
通过阅读本篇文章,如果有收获的话,可以点个赞,这将会成为我持续分享的动力,感谢~
我是五柳,喜欢创新、捣鼓源码,专注于源码(Vue 3、Vite)、前端工程化、跨端等技术学习和分享,欢迎关注我的微信公众号 Code center 或 GitHub。