vue 项目优化技巧之避免多余的深层递归响应式

591 阅读4分钟

前言

在 vue 项目中,那些被处理为响应式的数据,默认都是深度的。鉴于 Vue 3 的响应式系统使用了 Proxy 代理对象替代了 Vue 2 中的 Object.defineProperty(),所以我将分别针对 vue2 和 vue3 说说在处理超大型数组或层级很深的对象时,如何避免多余的深层递归响应式。

vue2

假设我们有个列表展示页,其中有 2 个列表:

  • list 列表的数据在点击“加载数据”按钮后,调用 load() 方法,然后在其内部调用 fetchList() 方法,直接将一个循环生成的有 10 万条数据的数组赋值给 list
  • freezeList 列表的数据在点击 “加载Freeze数据”按钮后,调用 loadFreeze() 方法,也是去获取 fetchList() 返回的数组,只不过会将该数组传给 Object.freeze() ,多进行了一步冻结的操作后再赋值给 freezeList

代码演示如下,点击 2 个按钮后,可以感觉后者的列表加载速度要稍微快一些:

如果在火狐浏览器运行并打开调试工具,在“性能”面板进行录制,分别点击 2 个按钮加载列表,结果如下。

直接赋值数组

调用 load() 函数时,js 的执行时间为 556 ms:

1.png

vue 除了会调用我们自己定义的 fetchList 之外还会调用 proxySetter,对新赋值给 list 的数据 —— 有 10 万个对象的数组 —— 做了响应式处理。这一过程耗时 542 ms:

2.png

load 中我打印查看了 list,可以看到数组中每个对象的属性都是设置了 getter 和 setter ,而且是递归处理的:

3.png

至于 fetchList 的执行,虽然是生成返回了 10 万个对象的数组,但仅仅耗时 8 ms:

4.png

如果我们确认获取的数组里的各个对象仅仅是展示而已,不会改变,那么 proxySetter 的操作,无疑是多余的深层递归响应式处理。

赋值冻结后的数组

调用 loadFreeze() 时,可以看到在调用树中,直接调用了 freezeList 而没有调用 proxySetter,从而运行耗时只有 10 ms:

5.png

loadFreeze 中打印查看 freezeList,可以看到其中的对象并没有被处理成响应式数据:

6.png

vue2 源码探究

为什么 freezeList 接收的 Object.freeze(this.fetchList()) 没有变为响应式的呢?查看 vue2 的源码可以得知,vue2 在处理对象属性的响应式时,会获取对象的属性描述符,如果 configurablefalse,就会直接 return

7.png

Object.freeze() 的作用之一就是将对象的所有现有属性的描述符的 configurable 特性更改为 falsethis.fetchList() 得到的是数组,数组也是对象。

我们可以通过 Object.isFrozen(this.freezeList) 查看 freezeList 是否被冻结,然后通过 Object.getOwnPropertyDescriptors 查看 freezeList 的所有自有属性描述符:

this.freezeList = Object.freeze(this.fetchList())
console.log(Object.isFrozen(this.freezeList)) // true
console.log(Object.getOwnPropertyDescriptors(this.freezeList))

可以看到数组中各个元素的 configurable 均为 false,自然也不会被进一步做响应式处理了:

8.png

vue3

在 vue3 项目中,虽然也可以通过 Object.freeze() 的方式来阻止对象的响应式处理,但面对上述案例的需求 —— 对于请求得到的数组数据,不对其内部的元素做深层递归地响应式处理,我们可以更方便地直接使用 vue3 提供的现有 api shallowRef() 来实现优化。代码示例如下:

使用 ref

点击“加载数据”调用的 load() 方法中,通过调用 fetchList() 获取的数组是传给了 Ref 类型的 list。通过谷歌浏览器的“性能”面板录制查看,可以发现 load 的执行时间为 14.9 毫秒:

1.png

如果我们打印 list 中的元素(以第 1 个元素为例):

function load() {
  list.value = fetchList()
  console.log(list.value[0])
}

可以看到其已经是个 Proxy 对象了:

2.png

使用 shallowRef

点击“加载Shallow数据”调用的 loadShallowList() 方法,其内部同样是调用 fetchList(),返回值不再通过 Object.freeze() 处理,而是直接赋值给 ShallowRef 类型的 shallowList,运行时间为 6.5 毫秒:

3.png

通过对比不难发现,loadShallowList 的运行速度快于 load,这是因为 ShallowRef 类型的数据,其内部值是原样存储和暴露的,vue 没有对数组中各个元素深层递归地转为响应式。打印查看 shallowList 的第 1 个元素:

function loadShallowList() {
  shallowList.value = fetchList()
  console.log(shallowList.value[0])
}

结果如下,只是个普通的对象:

4.png

markRaw

使用 Object.freeze() 我们可以让一个深层嵌套的对象的某个对象属性被冻结,进而控制该对象属性不被转为响应式的数据,在 vue3 中可以使用 markRaw() 来实现。比如让上例中 list 的第 1 个元素不要被转为 Proxy 对象:

import { markRaw } from 'vue'
function load() {
  const res = fetchList()
  markRaw(res[0])
  list.value = res
  console.log(list.value[0])
  console.log(list.value[1])
}

打印结果如下:

5.png

结语

有那么句名言:

“过早优化是万恶之源”(premature optimization is the root of all evil)—— Donald Knuth

本文提到的优化措施虽然让列表的加载速度提升了,但同时也使得列表中的元素失去了响应式,所以我们应该针对特定场景,谨慎地采用。

感谢.gif 点赞.png