众所周知,Vue 的特点之一就是响应式数据,Vue 会递归遍历所有 data 里的对象对它进行监听。这种模式在数据量大的时候会有较大的性能问题,这个时候我们就可以使用 Object.freeze 冻结一个对象,起到性能优化的作用。那么为什么 Object.freeze 可以做到呢?效果怎么呢?
一、性能优化实战场景
1.1 大数据列表渲染优化
我们先来看一个具体示例
xml
<template>
<div v-for="item in list" :key="item.id">{{ item.content }}</div>
</template>
<script>
// 未优化版本
export default {
data() {
return {
list: fetchLargeData() // 返回10万条普通对象
}
}
}
// 优化版本
export default {
data() {
return {
list: Object.freeze(fetchLargeData()) // 冻结数组及其元素
}
}
}
</script>
这是用谷歌的 Performance 面板基于上面代码测试出来的数据。第一张图是没有使用 Object.freeze,第二张图是使用了 Object.freeze。
可以看到 LCP 指标有明显的优化。
注:LCP (最大内容绘制时间),用于标记在可视窗口内最大的文本或图像元素被渲染到屏幕上的时间,通常是页面首屏中占据空间最大的元素。是衡量页面核心内容加载速度的关键指标。
二、为什么需要 Object.freeze()
2.1 Vue 响应式系统的代价
在 Vue 的响应式系统中,每个被观测的对象都会经过以下处理流程:
javascript
// 简化代码 实际代码会更加复杂
if (Object.isExtensible(value)) {
Object.defineProperty(obj, key, {
get() {
dep.depend();
return val;
},
set(newVal) {
val = newVal;
dep.notify();
},
});
// 响应式代理
// Object.defineProperty 遍历代理对象属性
}
每个属性都会被转换为访问器属性,产生以下性能影响:
- 每个属性需要创建独立的 Dep 实例( Dep 类负责管理依赖和触发更新,每一个响应式属性都有一个 dep)
- 数组需要特殊处理(重写数组方法)
- 嵌套对象需要递归处理
而在 Vue 源码中,会判断他是否为可扩展,如果是可扩展的,才遍历代理,这样能省去以上的步骤。优化性能。
2.2 Object.freeze 的冻结原理
通过 Object.freeze() 处理后的对象会:
javascript
const obj = { foo: 1 }
Object.freeze(obj)
// 等价于:
Object.defineProperty(obj, 'foo', {
writable: false, // 是否可以被修改
configurable: false // 是否可以被删除,以及除 value 和 writable 之外的其他特性是否可以被修改
})
Object.preventExtensions(obj) // 让一个对象变的不可扩展,也就是不能再为该对象添加新的属性
可以看到,Vue 源码中使用的是 Object.isExtensible 来决定是否监听对象,能实现这个效果的既可以使用 Object.preventExtensions ,也可以使用 Object.freeze 。
方法 | 作用 | 已有属性能否修改? | 已有属性能否删除? | 能否添加新属性? |
---|---|---|---|---|
Object.preventExtensions(obj) |
仅禁止添加新属性 | ✅ 可以 | ✅ 可以 | ❌ 禁止 |
Object.freeze(obj) |
禁止所有修改操作 | ❌ 不可 | ❌ 不可 | ❌ 禁止 |
2.3 Vue为何选择 isExtensible
而非 isFrozen
?
isFrozen: 判断对象是否冻结
isExtensible: 判断对象是否能扩展
- 覆盖所有不可变场景
- 具有最优性能
- 符合响应式系统的核心需求(只需确保对象可扩展即可安全代理)
2.4 开发建议
在 Vue2 中,确定这个对象不需要响应式的情况下 ,统一使用 Object.freeze
如需完全不可变的数据 (如配置对象),使用 Object.freeze
,并且避免在 data 函数里冻结
javascript
// config.js (在组件外部冻结)
const APP_CONFIG = Object.freeze({
apiBaseUrl: 'https://api.example.com',
features: Object.freeze({
analytics: true,
darkMode: false
})
})
// Vue 组件中使用
export default {
data() {
return {
config: APP_CONFIG // 不会被 Vue 响应式处理
}
},
methods: {
tryModify() {
this.config.apiBaseUrl = 'http://new.url' // 严格模式下会抛出 TypeError
}
}
}
- 重复冻结 :每次组件实例化时,
data
函数都会被调用,进而每次都会执行Object.freeze()
方法对新创建的对象进行冻结。这会造成不必要的性能损耗,尤其是在组件频繁创建和销毁的场景下,这种性能开销会更加明显。 - 资源浪费 :每次都创建新的对象并进行冻结,会占用更多的内存空间,因为每个组件实例都有自己独立的被冻结对象副本,而不是共享同一个对象
三、Vue 3 的响应式系统差异
3.1 Proxy 实现的特殊处理
Vue 3 的响应式处理逻辑:
javascript
// 简化代码 实际代码还有其他判断条件
function reactive(target) {
if (Object.isExtensible(target)) {
return target
}
// ...创建Proxy代理
const proxy = new Proxy(target)
proxyMap.set(target, proxy)
return proxy
}
// 如果对象是不可扩展的,直接返回,不进行响应式代理
vue3 中有提供 readonly
和 shallowReadonly
API,可以实现类似不可变数据的效果,同时与 Vue 的响应式系统深度集成,以下是对比示例
3.2. 推荐使用 readonly
的场景
csharp
// 场景:需要与响应式系统交互的不可变数据
import { ref, readonly } from 'vue'
const source = ref({ value: 1 })
const readOnlyView = readonly(source)
// 源数据变更时,只读视图同步更新
source.value.value = 2
console.log(readOnlyView.value.value) // 输出 2
3.2. 推荐使用 Object.freeze
的场景
javascript
// 场景:完全静态的巨型数据集(如 10万行表格数据)
const rawData = fetchBigData() // 返回普通对象
const frozenData = Object.freeze(rawData)
// 在 Vue 组件中使用
export default {
setup() {
return {
// 直接使用冻结数据,节省内存和初始化时间
bigData: frozenData
}
}
}