前言
在 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。