在 Vue 组件初始化数据时,会递归遍历在 data 中定义的每一条数据,通过Object.defineProperty
将数据改成响应式,这就意味着如果 data 中的数据量很大的话,在初始化时将会使用很长的时间去执行Object.defineProperty
, 也就会带来性能问题,这个时候我们可以强制使数据变为非响应式,从而节省时间,看下这个例子:
<template>
<div>
<ul>
<li v-for="item in heavyData" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
// 一万条数据
const heavyData = Array(10000)
.fill(null)
.map((_, idx) => ({ name: 'test', message: 'test', id: idx }))
export default {
data() {
return {
heavyData: heavyData
}
},
beforeCreate() {
this.start = Date.now()
},
created() {
console.log(Date.now() - this.start)
}
}
heavyData中有一万条数据,这里统计了下从beforeCreate
到created
经历的时间,对于这个例子而言,这个时间基本上就是初始化数据的时间。
这个时间一直在40-50ms,然后我们通过Object.freeze()
方法,将heavyData
变为非响应式的再试下:
data() {
return {
heavyData: Object.freeze(heavyData)
}
}
改完之后再试下,初始化数据的时间变成了0-1ms,快了有40ms,这40ms都是递归遍历heavyData执行Object.defineProperty的时间。
那么,为什么Object.freeze()
会有这样的效果呢?对某一对象使用Object.freeze()
后,将不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。
而 Vue 在将数据改造成响应式之前有个判断:
export function observe(value, asRootData) {
// ...省略其他逻辑
if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
// ...省略其他逻辑
}
这个判断条件中有一个Object.isExtensible(value)
,这个方法是判断一个对象是否是可扩展的,由于我们使用了Object.freeze()
,这里肯定就返回了false,所以就跳过了下面的过程,自然就省了很多时间。
实际上,不止初始化数据时有影响,你可以用上面的例子统计下从created到mounted所用的时间,在我的电脑上不使用Object.freeze()
时,这个时间是60-70ms,使用Object.freeze()
后降到了40-50ms,这是因为在渲染函数中读取heavyData
中的数据时,会执行到通过Object.defineProperty
定义的getter方法,Vue 在这里做了一些收集依赖的处理,肯定就会占用一些时间,由于使用了Object.freeze()
后的数据是非响应式的,没有了收集依赖的过程,自然也就节省了性能。
由于访问响应式数据会走到自定义 getter 中并收集依赖,所以平时使用时要避免频繁访问响应式数据,比如在遍历之前先将这个数据存在局部变量中,尤其是在计算属性、渲染函数中使用,关于这一点更具体的说明,你可以看黄奕老师的这篇文章:Local variables
但是这样做也不是没有任何问题的,这样会导致heavyData
下的数据都不是响应式数据,你对这些数据使用computed、watch等都不会产生效果,不过通常来说这种大量的数据都是展示用的,如果你有特殊的需求,你可以只对这种数据的某一层使用Object.freeze()
,同时配合使用上文中的延迟渲染、函数式组件等,可以极大提升性能。