vue3数据避免响应处理

410 阅读4分钟

前言

在我们请求数据后,有一些数据我们并不会用到,因此我们可以给他处理成非响应式(Non-reactive Object)的,以提高我们的系统的性能。

export default {
  data() {
    return {
      list: [
        {
          title: 'item1'
          msg: 'I am item1',
          extData: {
            type: 1
          }
        },
        ...
      ]
    }
  }
}
</script>

1、认识Reactivity Object 基础

在vue3中通过Reactivity Object创建响应式对象,是通过proxy创建原始响应对象和使用reflect对JavaScript代理。 通过vue3的ref,reactive,等api创建出来的对象都是响应式的。但有些数据我们并不需要他是响应式的,故从源码角度解决。

2、源码角度解决非响应式数据

首先,我们可以建立一个简单的认知,那就是对于 Non-reactivity Object 的处理肯定是是发生在创建响应式对象之前,我想这一点也很好理解。在源码中,创建响应式对象的过程则都是由 packages/reactivity/src/reactive.ts 文件中一个名为 createReactiveObject 的函数实现的。

2.1createReactiveObject

vue3创建响应式对象源码

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {}

createReactiveObject 函数总共会接收 5 个参数:

  • target 表示需要创建成响应式对象的原始对象
  • isReadonly 表示创建后的响应式对象是要设置为只读
  • baseHandlers 表示创建 Proxy 所需要的基础 handler,主要有 getsetdeletePropertyhasownKeys
  • collectionHandlers 表示集合类型(MapSet 等)所需要的 handler,它们会重写 adddeleteforEach 等原型方法,避免原型方法的调用中访问的是原始对象,导致失去响应的问题发生
  • proxyMap 表示已创建的响应式对象和原始对象的 WeekMap 映射,用于避免重复创建基于某个原始对象的响应式对象

然后,在 createReactiveObject 函数中则会做一系列前置的判断处理,例如判断 target 是否是对象、target 是否已经创建过响应式对象(下面统称为 Proxy 实例)等,接着最后才会创建 Proxy 实例。

那么,显然 Non-reactivity Object 的处理也是发生 createReactiveObject 函数的前置判断处理这个阶段的,其对应的实现会是这样(伪代码):

function createReactiveObject(...) {
  // ...
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // ...
}

当targetType等于TargetType.INVALID,我们就直接返回对象。

解析一下getTargetType(target)和TargetType.INVALID是什么

2.2getTargetType 和 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 逻辑。

结语

通过阅读我们可以避免嵌套对象的创建响应式过程,在某些情况下可以很好的优化性能。