前言
我们在使用 Vue 或其他框架的日常开发中,或多或少的都会遇到一些性能问题,尽管 Vue 内部已经帮助我们做了许多优化,但是还是有些问题是需要我们主动去避免的。我在我的日常开中,以及网上各种大佬的文章中总结了一些容易产生性能问题的场景以及针对这些问题优化的技巧,这篇文章就来探讨下,希望对你有所帮助。
使用v-slot:slotName
,而不是slot="slotName"
v-slot
是 2.6 新增的语法,具体可查看:Vue2.6,2.6 发布已经是快两年前的事情了,但是现在仍然有不少人仍然在使用slot="slotName"
这个语法。虽然这两个语法都能达到相同的效果,但是内部的逻辑确实不一样的,下面来看下这两种方式有什么不同之处。
我们先来看下这两种语法分别会被编译成什么:
使用新的写法,对于父组件中的以下模板:
<child>
<template v-slot:name>{
{name}}</template>
</child>
会被编译成:
function render() {
with (this) {
return _c('child', {
scopedSlots: _u([
{
key: 'name',
fn: function () {
return [_v(_s(name))]
},
proxy: true
}
])
})
}
}
使用旧的写法,对于以下模板:
<child>
<template slot="name">{
{name}}</template>
</child>
会被编译成:
function render() {
with (this) {
return _c(
'child',
[
_c(
'template',
{
slot: 'name'
},
[_v(_s(name))]
)
],
)
}
}
通过编译后的代码可以发现,旧的写法是将插槽内容作为 children 渲染的,会在父组件的渲染函数中创建,插槽内容的依赖会被父组件收集(name 的 dep 收集到父组件的渲染 watcher),而新的写法将插槽内容放在了 scopedSlots 中,会在子组件的渲染函数中调用,插槽内容的依赖会被子组件收集(name 的 dep 收集到子组件的渲染 watcher),最终导致的结果就是:当我们修改 name 这个属性时,旧的写法是调用父组件的更新(调用父组件的渲染 watcher),然后在父组件更新过程中调用子组件更新(prePatch => updateChildComponent),而新的写法则是直接调用子组件的更新(调用子组件的渲染 watcher)。
这样一来,旧的写法在更新时就多了一个父组件更新的过程,而新的写法由于直接更新子组件,就会更加高效,性能更好,所以推荐始终使用v-slot:slotName
语法。
使用计算属性
这一点已经被提及很多次了,计算属性最大的一个特点就是它是可以被缓存的,这个缓存指的是只要它的依赖的不发生改变,它就不会被重新求值,再次访问时会直接拿到缓存的值,在做一些复杂的计算时,可以极大提升性能。可以看以下代码:
<template>
<div>{
{superCount}}</div>
</template>
<script>
export default {
data() {
return {
count: 1
}
},
computed: {
superCount() {
let superCount = this.count
// 假设这里有个复杂的计算
for (let i = 0; i < 10000; i++) {
superCount++
}
return superCount
}
}
}
</script>
这个例子中,在 created、mounted 以及模板中都访问了 superCount 属性,这三次访问中,实际上只有第一次即created
时才会对 superCount 求值,由于 count 属性并未改变,其余两次都是直接返回缓存的 value
使用函数式组件
对于某些组件,如果我们只是用来显示一些数据,不需要管理状态,监听数据等,那么就可以用函数式组件。函数式组件是无状态的,无实例的,在初始化时不需要初始化状态,不需要创建实例,也不需要去处理生命周期等,相比有状态组件,会更加轻量,同时性能也更好。具体的函数式组件使用方式可参考官方文档:函数式组件
我们可以写一个简单的 demo 来验证下这个优化:
// UserProfile.vue
<template>
<div class="user-profile">{
{ name }}</div>
</template>
<script>
export default {
props: ['name'],
data() {
return {}
},
methods: {}
}
</script>
<style scoped></style>
// App.vue
<template>
<div id="app">
<UserProfile v-for="item in list" :key="item" : />
</div>
</template>
<script>
import UserProfile from './components/UserProfile'
export default {
name: 'App',
components: { UserProfile },
data() {
return {
list: Array(500)
.fill(null)
.map((_, idx) => 'Test' + idx)
}
},
beforeMount() {
this.start = Date.now()
},
mounted() {
console.log('用时:', Date.now() - this.start)
}
}
</script>
<style></style>
UserProfile 这个组件只渲染了 props 的 name,然后在 App.vue 中调用 500 次,统计从 beforeMount 到 mounted 的耗时,即为 500 个子组件(UserProfile)初始化的耗时。
经过我多次尝试后,发现耗时一直在 30ms 左右,那么现在我们再把改成 UserProfile 改成函数式组件:
<template functional>
<div class="user-profile">{
{ props.name }}</div>
</template>
此时再经过多次尝试后,初始化的耗时一直在 10-15ms,这些足以说明函数式组件比有状态组件有着更好的性能。
结合场景使用 v-show 和 v-if
以下是两个使用 v-show 和 v-if 的模板
<template>
<div>
<UserProfile :user="user1" v-if="visible" />
<button @click="visible = !visible">toggle</button>
</div>
</template>