前言
在 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
映射,用于避免重复创建基于某个原始对象的响应式对象
然后,在 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。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。