前言
- 什么时候需要优化性能
当你的项目规模扩大到一定程度,你会自然感受到项目运行明显缓慢。虽然在生产环境通常比开发环境流畅,但优化性能也可以提升开发效率,而不仅仅是为了生产环境下的体验。
- 前提准备
- 你需要先保证项目的规范化,这一步很重要,不规范的项目代码不仅仅会影响可读性,也有可能会影响运行速度,严重违反规范的代码甚至可能是性能低下的元凶。如果你还不知道怎么编写规范化的Vue,请查看我的往期文章。
- 你需要掌握一定的Vue渲染机制,这对于理解为什么会卡顿至关重要。这方面的文章已经数不胜数,但许多人学习渲染机制只是为了应付面试而死记硬背,这对优化性能没有帮助。因此确保你真正理解渲染机制很重要。
- 本文的演示代码将在Github公开,查看演示代码的运行结果在Github pages。
正文
Vue自身的优化已经足够高效,大多数情况下,Vue只会更新已经修改的部分,除了一些边缘情况。
组件会在他依赖的数据(出现在<template>
里的数据)发生更改时重新渲染。此时会对整个<template>
的内容重新渲染。
<script setup lang="ts">
import { ref } from 'vue'
const data1 = ref('data1')
const data2 = ref(1)
function timeConsumingProcessing(value: string) {
console.log('耗时操作正在执行')
// 模拟耗时操作
const start = Date.now()
while(Date.now() - start <= 1000) {
}
return value
}
const update = () => {
data2.value++
}
</script>
<template>
<div>
data1:{{ timeConsumingProcessing(data1) }}
</div>
<div>
data2:{{ data2 }}
</div>
<button @click="update">
update
</button>
</template>
在这个例子中,data2
的更新会导致整个组件的重新渲染,即使data1
没有更改。
使用computed对数据进行缓存
<script setup lang="ts">
import { computed, ref } from 'vue'
const data1 = ref('data1')
const data2 = ref(1)
function timeConsumingProcessing(value: string) {
console.log('耗时操作正在执行')
// 模拟耗时操作
const start = Date.now()
while(Date.now() - start <= 1000) {
}
return value
}
const processed = computed(() => timeConsumingProcessing(data1.value))
const update = () => {
data2.value++
}
</script>
<template>
<div>
data1:{{ processed }}
</div>
<div>
data2:{{ data2 }}
</div>
<button @click="update">
update
</button>
</template>
此时点击update,timeConsumingProcessing
将不会被调用。
但如果使用的是一个引用数据类型,更改了数据的引用,但不影响实际渲染的值。
<script setup lang="ts">
import { onBeforeUpdate, onUpdated, ref } from 'vue'
const data = ref({ key: 'value' })
const update = () => {
data.value = { key: 'value' }
}
function timeConsumingProcessing(value: string) {
// 模拟耗时操作
const start = Date.now()
while(Date.now() - start <= 1000) {
}
return value
}
onBeforeUpdate(() => {
console.log('组件开始更新')
})
onUpdated(() => {
console.log('组件已更新')
})
</script>
<template>
{{ timeConsumingProcessing(data.key) }}
<button @click="update">update</button>
</template>
这个例子中,修改了data
的引用,实际需要的data.key
并没有改变,但还是会触发组件的渲染.
再次使用computed对数据进行缓存
<script setup lang="ts">
import { computed, onBeforeUpdate, onUpdated, ref } from 'vue'
const data = ref({ key: 'value' })
const update = () => {
data.value = { key: 'value' }
}
function timeConsumingProcessing(value: string) {
console.log('耗时操作正在执行')
// 模拟耗时操作
const start = Date.now()
while(Date.now() - start <= 1000) {
}
return value
}
const processed = computed(() => timeConsumingProcessing(data.value.key))
onBeforeUpdate(() => {
console.log('组件开始更新')
})
onUpdated(() => {
console.log('组件已更新')
})
</script>
<template>
{{ processed }}
<button @click="update">update</button>
</template>
此时再点击,模板不渲染了,但是耗时操作还是会执行。这是因为processed
依赖data
,所以data
更新后就会重新执行timeConsumingProcessing
,但processed
没有更改,所以Vue认为不需要更新组件,就跳过了模板渲染。
computed
会在其依赖的数据发生任何更改后重新执行,而不关心数据的更改是否会影响结果
这样的优化是失败的,我们的目的应该是阻止timeConsumingProcessing
被重复调用,这才是耗时的部分。
使用watch
处理数据
<script setup lang="ts">
import { onBeforeUpdate, onUpdated, ref, watch } from 'vue'
const data1 = ref('1')
const data = ref({ key: 'value' })
const update = () => {
data.value = { key: 'value' }
data1.value = '1'
}
function timeConsumingProcessing(value: string) {
console.log('耗时操作正在执行')
// 模拟耗时操作
const start = Date.now()
while(Date.now() - start <= 1000) {
}
return value
}
const processed = ref('value')
watch(() => data1.value, () => {
processed.value = timeConsumingProcessing(data1.value)
}, {
immediate: true,
})
onBeforeUpdate(() => {
console.log('组件开始更新')
})
onUpdated(() => {
console.log('组件已更新')
})
</script>
<template>
{{ processed }}
<button @click="update">update</button>
</template>
这个例子中,使用() => data1.value
显式地告诉Vue,我需要在data1.value
发生改变的时候更新processed
。
但这并不是一个好的办法,watch
是一个风险很高的API,他会导致数据流向难以预测,可读性极差。大量使用watch
也很容易出现意外的BUG,而这样的BUG一旦出现,调试会非常麻烦。我的建议是永远不要用watch
来做数据的懒处理。
分割组件
创建一个新的组件
- Field.vue
<script setup lang="ts">
defineProps<{
value: string
}>()
function timeConsumingProcessing(value: string) {
console.log('耗时操作正在执行')
// 模拟耗时操作
const start = Date.now()
while(Date.now() - start <= 1000) {
}
return value
}
</script>
<template>
{{ timeConsumingProcessing(value) }}
</template>
- ReferenceData.vue
<script setup lang="ts">
import { onBeforeUpdate, onUpdated, ref } from 'vue'
import Field from './Field.vue'
const data1 = ref('1')
const data = ref({ key: 'value' })
const update = () => {
data.value = { key: 'value' }
data1.value = '1'
}
onBeforeUpdate(() => {
console.log('组件开始更新')
})
onUpdated(() => {
console.log('组件已更新')
})
</script>
<template>
<Field :value="data.key"/>
<button @click="update">update</button>
</template>
这个例子中,点击update,因为Field
组件的Props没有改变,被跳过渲染,所以耗时操作不会被执行。
分割组件在这里扮演的作用类似于computed
,但比computed
拥有更强大的功能。
在Vue的项目中,大部分的性能问题都是由于在一个组件里塞进去了太多东西,我见过很多项目里的单个vue文件达到几千行,这不但影响可读性,也有可能对性能造成很大影响。
何时分割组件是接下来重点关注的部分,在规范化的文章里我建议在单个文件过大时始终分割组件,但在这里我会会讨论在什么地方分割组件的优化效果最好。
- 将不相关的数据分割到不同的组件里。
如果一个组件有两个互不相关的数据,在一个组件里,任何一个数据的修改会触发整个组件的渲染。
将他们分割到独立的组件中,这样当一个组件的数据发生改变,另一个组件不会触发渲染。
2.将v-for
的元素分割到单独的组件里。
v-for
在性能优化里有着举足轻重的地位,因为v-for
依赖的是一个数组或者对象(引用数据类型),这也导致它经常发生不必要的渲染。而且v-for
对性能的影响会随着数组的长度直线上升。
很多人对v-for
有误解,认为只有发生改变的元素才会重新渲染,而实际上任何一个元素的任何属性发生改变,都将导致整个循环重新渲染。
<script setup lang="ts">
import { ref } from 'vue'
const list = ref([
{
data: 1,
},
{
data: 2,
},
{
data: 3,
},
])
function timeConsumingProcessing(value: number) {
console.log('耗时操作正在执行')
// 模拟耗时操作
const start = Date.now()
while(Date.now() - start <= 1000) {
}
return value
}
const update = () => {
list.value[0].data++
}
</script>
<template>
<div>
<div v-for="item in list">
{{ timeConsumingProcessing(item.data) }}
</div>
</div>
<button @click="update">
update
</button>
</template>
在这个例子中,点击update,timeConsumingProcessing
将被调用三次,直到三秒后渲染才会完成。
为了避免不必要的渲染,将v-for
的元素提取成为独立的组件很有必要。
- ForListItem.vue
<script setup lang="ts">
defineProps<{
data: number
}>()
function timeConsumingProcessing(value: number) {
console.log('耗时操作正在执行')
// 模拟耗时操作
const start = Date.now()
while(Date.now() - start <= 1000) {
}
return value
}
</script>
<template>
{{ timeConsumingProcessing(data) }}
</template>
<style scoped>
</style>
- ForList.vue
<script setup lang="ts">
import { ref } from 'vue'
import ForListItem from './ForListItem.vue'
const list = ref([
{
data: 1,
},
{
data: 2,
},
{
data: 3,
},
])
const update = () => {
list.value[0].data++
}
</script>
<template>
<div>
<div v-for="item in list">
<ForListItem :data="item.data"/>
</div>
</div>
<button @click="update">
update
</button>
</template>
我知道有很多人在问computed
怎么传递参数,实际上是computed
根本不能传递参数,也完全不需要。一个组件就有computed
的所有功能了,可以说computed
就是为“懒人”准备的语法糖,它让开发者可以简单的减少耗时处理方法的调用次数。所以从设计上computed
就不能有更复杂的功能,否则他就脱离了存在的初衷。
Vue深度性能优化系列正在更新...