乐趣区

关于前端:揭秘Vue3-性能优化之-Nonreactive-Object

前言

在 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.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}
  }
])

能够看到,咱们用 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,次要有 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}
  // ...
}

能够看到,只有应用 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 返回 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
}

或者:

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 示意汇合类型,MapSetWeakMapWeakSet
  • TargetType.INVALID 示意不非法的类型,不是对象、数组、汇合

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

const enum TargetType {
  INVALID = 0,
  COMMON = 1,
  COLLECTION = 2
}

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

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

结语

浏览到这里,我想大家都明确了如何在创立一个简单对象的响应式对象的时候,跳过对象中一些嵌套对象的创立响应式的过程。并且,这个小技巧在某些场景下,不可否认的是一个很好的优化伎俩,所以提前做好必要的认知也是很重要的。

最初,如果文中存在表白不当或谬误的中央,欢送各位同学提 Issue ~

点赞

通过浏览本篇文章,如果有播种的话,能够 点个赞,这将会成为我继续分享的能源,感激~

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

退出移动版