前言
在 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:
vue 除了会调用我们自己定义的 fetchList 之外还会调用 proxySetter,对新赋值给 list 的数据 —— 有 10 万个对象的数组 —— 做了响应式处理。这一过程耗时 542 ms:
在 load 中我打印查看了 list,可以看到数组中每个对象的属性都是设置了 getter 和 setter ,而且是递归处理的:
至于 fetchList 的执行,虽然是生成返回了 10 万个对象的数组,但仅仅耗时 8 ms:
如果我们确认获取的数组里的各个对象仅仅是展示而已,不会改变,那么 proxySetter 的操作,无疑是多余的深层递归响应式处理。
赋值冻结后的数组
调用 loadFreeze() 时,可以看到在调用树中,直接调用了 freezeList 而没有调用 proxySetter,从而运行耗时只有 10 ms:
在 loadFreeze 中打印查看 freezeList,可以看到其中的对象并没有被处理成响应式数据:
vue2 源码探究
为什么 freezeList 接收的 Object.freeze(this.fetchList()) 没有变为响应式的呢?查看 vue2 的源码可以得知,vue2 在处理对象属性的响应式时,会获取对象的属性描述符,如果 configurable 为 false,就会直接 return:
Object.freeze() 的作用之一就是将对象的所有现有属性的描述符的 configurable 特性更改为 false。this.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,自然也不会被进一步做响应式处理了:
vue3
在 vue3 项目中,虽然也可以通过 Object.freeze() 的方式来阻止对象的响应式处理,但面对上述案例的需求 —— 对于请求得到的数组数据,不对其内部的元素做深层递归地响应式处理,我们可以更方便地直接使用 vue3 提供的现有 api shallowRef() 来实现优化。代码示例如下:
使用 ref
点击“加载数据”调用的 load() 方法中,通过调用 fetchList() 获取的数组是传给了 Ref 类型的 list。通过谷歌浏览器的“性能”面板录制查看,可以发现 load 的执行时间为 14.9 毫秒:
如果我们打印 list 中的元素(以第 1 个元素为例):
function load() {
list.value = fetchList()
console.log(list.value[0])
}
可以看到其已经是个 Proxy 对象了:
使用 shallowRef
点击“加载Shallow数据”调用的 loadShallowList() 方法,其内部同样是调用 fetchList(),返回值不再通过 Object.freeze() 处理,而是直接赋值给 ShallowRef 类型的 shallowList,运行时间为 6.5 毫秒:
通过对比不难发现,loadShallowList 的运行速度快于 load,这是因为 ShallowRef 类型的数据,其内部值是原样存储和暴露的,vue 没有对数组中各个元素深层递归地转为响应式。打印查看 shallowList 的第 1 个元素:
function loadShallowList() {
shallowList.value = fetchList()
console.log(shallowList.value[0])
}
结果如下,只是个普通的对象:
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])
}
打印结果如下:
结语
有那么句名言:
“过早优化是万恶之源”(premature optimization is the root of all evil)—— Donald Knuth
本文提到的优化措施虽然让列表的加载速度提升了,但同时也使得列表中的元素失去了响应式,所以我们应该针对特定场景,谨慎地采用。