一、按需加载组件
在首屏中存在大量不会显示的组件,比如商品营销弹窗、商品规格弹窗、活动规则弹窗等,虽然没有使用,但是也被一起加载进来,导致 js 文件体积过大加载缓慢,代码利用率低下。
使用
import()动态导入,配合Prefetching食用更香 Webpack Code Splitting。
1. Vue 中的异步组件
利用 v-if 的特性,在真正使用 Dialog 的时候才去加载代码,否则初始化渲染的时候就会立即加载没有使用到的组件。
<template>
<div class="page">
<!-- bad -->
<Dialog :visible.sync="visible" />
<!-- good -->
<Dialog v-if="canLoad" :visible.sync="visible" />
<button @click="showDialog">点击我显示 Dialog </button>
</div>
<template>
export default {
data () {
return: {
canLoad: false,
visible: false
}
},
methods: {
showDialog () {
this.canLoad = true
this.visible = true
}
},
components: {
Dialog: () => import('./components/Dialog')
}
}
2. component Vue 内置的动态组件
props: { is: string | ComponentDefinition | ComponentConstructor }
<template>
<div class="page">
<component :is="dialog" :visible.sync="visible" />
<button @click="showDialog">点击我显示 Dialog </button>
</div>
<template>
export default {
data () {
return: {
dialog: null,
visible: false
}
},
methods: {
showDialog () {
import('./components/Dialog').then(res => {
this.dialog = res.default
this.visible = true
// or
// this.dialog = Vue.extend(res.default)
})
}
}
}
3. Vue.extend
// dialog.js
import Vue from 'vue'
const Dialog = () => import('./Dialog.vue')
let instance = null
export const showDialog = options => {
if (instance) {
instance.visibile = true
return
}
Dialog()
.then(res => {
if (!instance) {
const AgreementConstructor = Vue.extend(res.default)
instance = new AgreementConstructor({
el: document.createElement('div')
})
instance.close = url => {
instance.visibile = false
}
}
document.body.appendChild(instance.$el)
instance.visibile = true
})
}
二、使用函数式组件
使组件无状态 (没有
data) 和无实例 (没有this上下文)。他们用一个简单的render函数返回虚拟节点使它们渲染的代价更小。
1. 单文件组件
<template functional>
<div @click="listeners.hello">
{{ props.msg }}
</div>
</template>
2. JSX
export default {
components: {
HelloWorld: {
functional: true,
props: {
msg: {
type: String,
default: 'Hello'
}
},
render (_, context) {
return (
<div onClick={ context.listeners.hello }>
<p>{ context.props.msg }</p>
</div>
)
}
}
}
}
三、减少 this 的访问次数
1. 在 computed 中
export default {
// ...
computed: {
// bad
fullName () {
return this.firstName + ' ' + this.lastName
},
// good
fullName ({ firstName, lastName }) {
return firstName + ' ' + lastName
}
}
}
2. 在 methods 中
export default {
// ...
methods: {
// bad
fullName () {
return this.firstName + ' ' + this.lastName
},
// good
fullName ({ firstName, lastName }) {
const { firstName, lastName } = this
return firstName + ' ' + lastName
}
}
}
四、合理使用生命周期
1. 解绑事件
export default {
// bad
created () {
window.addEventListener('scroll', this.scroll)
}
beforeDestroy () {
window.removeEventListener('scroll', this.scroll)
}
// good
created () {
window.addEventListener('scroll', this.scroll)
this.$on('hook:beforeDestroy', () => {
window.removeEventListener('scroll', this.scroll)
})
}
}
2. 代码拆分
export default {
created: [
function () { // task1.. },
function () { // task2.. },
function () { // task3.. },
// ...
]
}
五、Data & Watch
1. 尽量减少 data 中数据的嵌套深度
data 中对象嵌套过深,Vue 初始化时需要递归进行响应式数据绑定,影响性能,白屏时间长。
export default {
data () {
return {
// bad
data: { obj: { obj: { name: '星星' } } },
// good
data: null
}
},
created () {
this.ajax()
},
methods: {
ajax () {
// 发送 ajax
this.data = { /** ajax 返回值 **/ }
}
}
}
2. 使用 Object.freeze()
Object.freeze()方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。
Vue 中使用 Object.isExtensible() 判断一个对象是否是可扩展的(是否可以在它上面添加新的属性),不可扩展就不会对数据进行响应式绑定(不会遍历数据的每个 key 进行劫持),适合展示类的场景。
export default {
data () {
return {
data: Object.freeze({ name: '星星' })
}
},
methods: {
setData () {
// 报错
this.data.name = 'star'
// 新的响应式对象
this.data = { name: 'star' }
// 新的冻结对象
this.data = Object.freeze({ name: 'star' })
}
}
}
3. 尽量减少在 watch 中使用 deep
deep 选项开启时,Vue 初始化时需要递归进行依赖收集,影响性能,白屏时间长。
推荐使用键路径。
export default {
data () {
return {
data: { obj: { obj: { name: '星星' } } },
}
},
watch: {
// bad
data: {
deep: true,
handler (val) {}
},
// good
// 键路径
'data.obj.obj.name' (val) {} // 不会递归
}
}
六、组件化
父组件状态变化,所有子组件会创建新的 VNode,并且进行 diff 操作,当 this.getProduct 执行时更新了状态,banner 列表会进行无用 diff,造成浪费。
子组件内部状态变化,不会影响父组件。
// bad
<template>
<div>
<div class="banner">
<div v-for="banner in banners">...</div>
</div>
<div class="product">
<div v-for="product in products">...</div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
banners: [],
products: []
}
},
created () {
this.getBanner()
setTimeout(this.getProduct, 1000)
}
methods: {
getBanner () {
// ajax...
this.banners = [ ... ]
},
getProduct () {
// ajax...
this.products = [ ... ]
}
}
}
</script>
// good
<template>
<div>
<!-- 内部维护状态,互不影响 -->
<banner></banner>
<product></product>
</div>
</template>
<script>
// ...
export default {
components: {
Banner,
Product
}
}
</script>
总结
本文介绍了几种 Vue 代码层面的优化,我会不断补充其他问题及优化措施,希望对读完本文的你有帮助、有启发,如果有不足之处,欢迎批评指正交流!