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


    前言

    在 Vue2 中,有一个老生常谈的话题,如何避免 data 中一个复杂对象(自身或属性对象)被默认被创建为响应式(Non-reactive Object)的过程? 举个例子,有一个 Vue2 的组件的 data

    <script>
    export default {
      data() {
        return {
          list: [
            {
              title: 'item1'
              msg: 'I am item1',
              extData: {
                type: 1
              }
            },
            ...
          ]
        }
      }
    }
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这里我们希望 list.extData 不被创建为响应式的对象,相信很多同学都知道,我们可以通过 Object.defineProperty 设置对象 list.extDataconfigurable 属性为 false 来实现。

    而在 Vue2 中,我们可以这么做,但是回到 Vue3,同样的问题又要怎么解决呢? 我想这应该是很多同学此时心中持有的疑问。所以,下面让我们一起来由浅至深地去解开这个问题。

    1 认识 Reactivity Object 基础

    首先,我们先来看一下 Reactivity Object 响应式对象,它是基于使用 Proxy 创建一个原始对象的代理对象和使用 Reflect代理 JavaScript 操作方法,从而完成依赖的收集和派发更新的过程。

    然后,我们可以根据需要通过使用 Vue3 提供的 refcomputereactivereadonly 等 API 来创建对应的响应式对象。

    这里,我们来简单看个例子:

    import { reactive } from '@vue/reactivity'
    const list = reactive([
      {
        title: 'item1'
        msg: 'I am item1',
        extData: {
          type: 1
        }
      }
    ])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以看到,我们用 reactive 创建了一个响应式数据 list。并且,在默认情况下 list 中的每一项中的属性值为对象的都会被处理成响应式的,在这个例子就是 extData,我们可以使用 Vue3 提供的 isReactive 函数来验证一下:

    console.log(`extData is reactive: ${isReactive(list[0].extData)}`)
    // 输出 true
    
    • 1
    • 2

    控制台输出:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HAH6wZ3M-1659854431664)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/28dd50d2b8ca4ef7a2bf06722342b514~tplv-k3u1fbpfcp-zoom-1.image)]

    可以看到 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>
    ) {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可以看到 createReactiveObject 函数总共会接收 5 个参数,我们分别来认识这 5 个函数形参的意义:

    • target 表示需要创建成响应式对象的原始对象
    • isReadonly 表示创建后的响应式对象是要设置为只读
    • baseHandlers 表示创建 Proxy 所需要的基础 handler,主要有 getsetdeletePropertyhasownKeys
    • collectionHandlers 表示集合类型(MapSet 等)所需要的 handler,它们会重写 adddeleteforEach 等原型方法,避免原型方法的调用中访问的是原始对象,导致失去响应的问题发生
    • 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
      }
      // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以看到,只要使用 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))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    其中 getTargetType 主要做了这 3 件事:

    • 判断 target 上存在 ReactiveFlags.SKIP 属性,它是一个字符串枚举,值为 __v_ship,存在则返回 TargetType.INVALID
    • 判断 target 是否可扩展 Object.isExtensible 返回 truefalse,为 true 则返回 TargetType.INVALID
    • 在不满足上面 2 者的情况时,返回 targetTypeMap(toRawType(value))

    从 1、2 点可以得出,只要你在传入的 target 上设置了 __v_ship 属性、或者使用 Object.preventExtensionsObject.freezeObject.seal 等方式设置了 target 不可扩展,那么则不会创建 target 对应的响应式对象,即直接返回 TargetType.INVALIDTargetType 是一个数字枚举,后面会介绍到)。

    在我们上面的这个例子就是设置 extData

    {
      type: 1,
      __v_ship: true
    }
    
    • 1
    • 2
    • 3
    • 4

    或者:

    Object.freeze({
      type: 1
    })
    
    • 1
    • 2
    • 3

    那么,在第 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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后,接着是 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
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    可以看到,targetTypeMap 函数实际上是对我们所认识的数据类型做了 3 个分类:

    • TargetType.COMMON 表示对象 Object、 数组Array
    • TargetType.COLLECTION 表示集合类型,MapSetWeakMapWeakSet
    • TargetType.INVALID 表示不合法的类型,不是对象、数组、集合

    其中,TargetType 对应的枚举实现:

    const enum TargetType {
      INVALID = 0,
      COMMON = 1,
      COLLECTION = 2
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    那么,回到我们上面的这个例子,由于 list.extDatatoRawType 函数中返回的是数组 Array,所以 targetTypeMap 函数返回的类型则会是 TargetType.COMMON(不等于 TargetType.INVALID),也就是最终会为它创建响应式对象。

    因此,在这里我们可以得出一个结论,如果我们需要跳过创建响应式对象的过程,则必须让 target 满足 value[ReactiveFlags.SKIP] || !Object.isExtensible(value) 或者命中 targetTypeMap 函数中的 default 逻辑。

    结语

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

    最后,如果文中存在表达不当或错误的地方,欢迎各位同学提 Issue ~

    点赞

    通过阅读本篇文章,如果有收获的话,可以点个赞,这将会成为我持续分享的动力,感谢~

    我是五柳,喜欢创新、捣鼓源码,专注于源码(Vue 3、Vite)、前端工程化、跨端等技术学习和分享,欢迎关注我的微信公众号 Code centerGitHub

  • 相关阅读:
    倍福PLC实现绝对值编码器原点断电保持---bias的使用
    【MySQL × SpringBoot 突发奇想】全面实现流程 · 数据库导出Excel表格文件的接口
    0076 稀疏数组
    你不知道的JavaScript-----强制类型转换
    机器学习笔记之最优化理论与方法(六)无约束优化问题——最优性条件
    基于JavaSwing开发自选拼图游戏 课程设计 大作业
    操作系统——调度算法
    minio策略配置参数详解
    eval()方法字符串转对象; 分别取对象属性名和属性的方法
    微信小程序开发---小程序的页面配置
  • 原文地址:https://blog.csdn.net/qq_42049445/article/details/126211034