揭秘,Vue3 性能优化之 Non-reactive Object

更新日期: 2022-08-08 阅读: 1.4k 标签: Vue3

前言

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)}`)

控制台输出:

图片

可以看到 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 逻辑。

结语

阅读到这里,我想大家都明白了如何在创建一个复杂对象的响应式对象的时候,跳过对象中一些嵌套对象的创建响应式的过程。并且,这个小技巧在某些场景下,不可否认的是一个很好的优化手段,所以提前做好必要的认知也是很重要的。

以上文章来源于Code center ,作者五柳

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/11980

相关推荐

Vue3 Hook 到底是啥黑魔法?

早就听说,React社区,已经全面拥抱Hook。Vue3的发布也支持了自定义Hook,作为只会Vue的前端小码农自然要去看看Vue3 Hook到底是啥黑魔法?

快速进阶Vue3.0

在2019.10.5日发布了Vue3.0预览版源码,但是预计最早需要等到 2020 年第一季度才有可能发布 3.0 正式版。新版Vue 3.0计划并已实现的主要架构改进和新功能:

vue3在setup中通过$ref获取dom元素

在使用vue2的时候,我们需要获取dom元素,或者获取组件的相关方法属性,一般都是通过this.$refs[domName]的方式,但是在vue3的setup中是没有this的,那么如何获取$refs呢?

vue3对比vue2使用,代码解释最直观

对于大多数组件,Vue2和Vue3中的代码即使不完全相同,也是非常相似的。但是,Vue3支持片段,这意味着组件可以有多个根节点。这在呈现列表中组件以删除不必要的包装器div元素时特别有用。但是,在本例中,表单组件的两个版本都将只保留一个根节点

浅谈Vue3的watchEffect用途

vue2里面的 watch api 大家应该都挺熟悉的了, vue2中vue实例里面有一个 $watch 方法 在sfc(sigle file component)里面有一个 watch 选项。他可以实现在一个属性变更的时候,去执行我们想要的行为

从 Proxy 到 Vue 源码,深入理解 Vue 3.0 响应系统

10 月 5 日,尤雨溪在 GitHub 开放了 Vue 3.0 处于 pre-alpha 状态的源码,这次 Vue 3.0 Updates 版本的更新,将带来五项重大改进:速度体积、可维护性、面向原生、易用性

Vue3数据响应系统

Vue3 就是基于 Proxy 对其数据响应系统进行了重写,现在这部分可以作为独立的模块配合其他框架使用。数据响应可分为三个阶段: 初始化阶段 --> 依赖收集阶段 --> 数据响应阶段

在Vue2与Vue3中构建相同的组件

Vue 开发团队终于在今天发布了 3.0-beta.1 版本,也就是测试版。通常来说,从测试版到正式版,只会修复 bug,不会引入新功能,或者删改老功能。所以,如果你对新版本非常感兴趣,或者有新项目即将上马,不妨尝试一下新版本

200 行从零实现 vue3

emmm 用半天时间捋顺了 vue3 的源码,再用半天时间写了个 mini 版……我觉得我也是没谁了,vue3 的源码未来一定会烂大街的,我们越早的去复现它,就……emm可以越早的装逼hhh

Vue 的数据响应式(Vue2 及 Vue3)

从一开始使用 Vue 时,对于之前的 jq 开发而言,一个很大的区别就是基本不用手动操作 dom,data 中声明的数据状态改变后会自动重新渲染相关的 dom。换句话说就是 Vue 自己知道哪个数据状态发生了变化及哪里有用到这个数据需要随之修改。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!