这 22 个 Vue3 的实用技巧,你可能还不知道!
以下内容来自公众号逆锋起笔,关注每日干货及时送达
作者:月夕
链接:https://juejin.cn/post/7005880217684148231
演示代码使用 Vue3 + ts + Vite 编写,但是也会列出适用于 Vue2 的优化技巧,如果某个优化只适用于 Vue3 或者 Vue2,我会在标题中标出来。
一、代码优化
v-for 中使用 key
v-for
更新已渲染的元素列表时,默认用就地复用策略;列表数据修改的时候,他会根据 key
值去判断某个值是否修改,如果修改,则重新渲染这一项,否则复用之前的元素;不要使用可能重复的或者可能变化 key
值(控制台也会给出提醒)如果数组中的数据有状态需要维持时(例如输入框),不要使用数组的 index
作为key
值,因为如果在数组中插入或者移除一个元素时,其后面的元素 index 将会变化,这回让vue进行原地复用时错误的绑定状态。如果数组中没有唯一的 key值可用,且数组更新时不是全量更新而是采用类似push,splice来插入或者移除数据时,可以考虑对其添加一个 key 字段,值为 Symbol() 即可保证唯一。
何时使用何种key?
原地复用
(大概就是 虚拟dom
变化时,两个 虚拟dom节点
的 key
如果一样就不会重新创建节点,而是修改原来的节点)index
作为 key
再好不过,因为进入下一页或者上一页时就会原地复用之前的节点,而不是重新创建,如果使用唯一的 id
作为 key
反而会重新创建dom,性能相对较低。index
作为 key
我还应该要尽量避免对数组的中间进行 增加/删除 等会影响后面元素key变化的操作。这会让 vue 认为后面所有元素都发生了变化,导致多余的对比和原地复用。数据没有独立的状态 数据不会进行 增加/删除 等会影响后面元素key变化的操作
哪何时使用 id 作为 key 呢?
id
都是唯一的,这无疑的一个 key
的优选答案。对于任何大多数情况使用 id
作为 key
都不会出现上面 bug
。但是如果你需要考虑性能问题,那就就要思考是否应该使用原地复用了。id
作为 key
,可想而知每一页的每一条数据 id
都是不一样的,所以当换页时两颗 虚拟DOM树
的节点的 key
完全不一致,vue
就会移除原来的节点然后创建新的节点。可想而知效率会更加低下。但是他也有它的优点。唯一的 key
可以帮助 diff
更加精确的为我们绑定状态,这尤其适合数据有独立的状态的场景,例如带输入框或者单选框的列表数据。id
作为 key
?只有一点:无法使用 index
作为key
的时候
v-if/v-else-if/v-else 中使用 key
可能很多人都会忽略这个点
如果只有一个 v-if ,没有 v-else 或者 v-if-else的话,就没有必要加 key 了
<transition>
<button
v-if="isEditing"
v-on:click="isEditing = false"
>
Save
button>
<button
v-else
v-on:click="isEditing = true"
>
Edit
button>
transition>
.v-enter-active, .v-leave-active {
transition: all 1s;
}
.v-enter, .v-leave-to {
opacity: 0;
transform: translateY(30px);
}
.v-leave-active {
position: absolute;
}
v-for 和 v-if 不要一起使用(Vue2)
此优化技巧仅限于Vue2,Vue3 中对 v-for 和 v-if 的优先级做了调整
永远不要把 v-if
和v-for
同时用在同一个元素上。 引至 Vue2.x风格指南[1]
v-for
的 优先级高于 v-if
,所以当它们使用再同一个标签上是,每一个渲染都会先循环再进行条件判断注意: Vue3 中 v-if
优先级高于v-for
,所以当v-for
和v-if
一起使用时效果类似于Vue2
中把v-if
上提的效果
Vue2
中是不被推荐的,Vue
也会给出对应的警告<ul>
<li v-for="user in users" v-if="user.active">
{{ user.name }}
li>
ul>
v-if
移动到上级 或者 使用 计算属性来处理数据<ul v-if="active">
<li v-for="user in users">
{{ user.name }}
li>
ul>
template
来作为其父元素,template
不会被浏览器渲染为 DOM
节点computed
来对遍历对象进行过滤// js
let usersActive = computed(()=>users.filter(user => user.active))
// template
<ul>
<li v-for="user in usersActive">
{{ user.name }}
li>
ul>
合理的选择 v-if 和 v-show
v-if
和 v-show
的区别相比大家都非常熟悉了;v-if
通过直接操作 DOM 的删除和添加来控制元素的显示和隐藏;v-show
是通过控制 DOM 的 display
CSS熟悉来控制元素的显示和隐藏v-show
来提高性能。v-if
来移除 DOM 可以节约掉浏览器渲染这个的一部分DOM需要的资源使用简单的 计算属性
应该把复杂计算属性分割为尽可能多的更简单的 property。
易于测试
当每个计算属性都包含一个非常简单且很少依赖的表达式时,撰写测试以确保其正确工作就会更加容易。
易于阅读
简化计算属性要求你为每一个值都起一个描述性的名称,即便它不可复用。这使得其他开发者 (以及未来的你) 更容易专注在他们关心的代码上并搞清楚发生了什么。
更好的“拥抱变化”
任何能够命名的值都可能用在视图上。举个例子,我们可能打算展示一个信息,告诉用户他们存了多少钱;也可能打算计算税费,但是可能会分开展现,而不是作为总价的一部分。微信搜索readdot,关注后回复视频教程获取23种精品资料
小的、专注的计算属性减少了信息使用时的假设性限制,所以需求变更时也用不着那么多重构了。
引至 Vue2风格指南[2]
let price = computed(()=>{
let basePrice = manufactureCost / (1 - profitMargin)
return (
basePrice -
basePrice * (discountPercent || 0)
)
})
let basePrice = computed(() => manufactureCost / (1 - profitMargin))
let discount = computed(() => basePrice * (discountPercent || 0))
let finalPrice = computed(() => basePrice - discount)
computed
的缓存特性
,不会重新计算 basePricefunctional 函数式组件(Vue2)
注意,这仅仅在 Vue2 中被作为一种优化手段,在 3.x 中,有状态组件和函数式组件之间的性能差异已经大大减少,并且在大多数用例中是微不足道的。因此,在 SFCs 上使用 functional
的开发人员的迁移路径是删除该 attribute,并将props
的所有引用重命名为$props
,将attrs
重命名为$attrs
。
<template>
<div class="cell">
<div v-if="value" class="on">div>
<section v-else class="off">section>
div>
template>
<script>
export default {
props: ['value'],
}
script>
<template functional>
<div class="cell">
<div v-if="props.value" class="on">div>
<section v-else class="off">section>
div>
template>
<script>
export default {
props: ['value'],
}
script>
没有this(没有实例) 没有响应式数据
拆分组件
<template>
<div :style="{ opacity: number / 300 }">
<div>{{ heavy() }}div>
div>
template>
<script>
export default {
props: ['number'],
methods: {
heavy () { /* HEAVY TASK */ }
}
}
script>
<template>
<div :style="{ opacity: number / 300 }">
<ChildComp/>
div>
template>
<script>
export default {
props: ['number'],
components: {
ChildComp: {
methods: {
heavy () { /* HEAVY TASK */ }
},
render (h) {
return h('div', this.heavy())
}
}
}
}
script>
ChildComp
却不会重新渲染,因为它的内部也没有任何响应式数据的变化。所以优化后的组件不会在每次渲染都执行耗时任务使用局部变量
<template>
<div :style="{ opacity: start / 300 }">{{ result }}div>
template>
<script>
import { heavy } from '@/utils'
export default {
props: ['start'],
computed: {
base () { return 42 },
result () {
let result = this.start
for (let i = 0; i < 1000; i++) {
result += heavy(this.base)
}
return result
}
}
}
script>
<template>
<div :style="{ opacity: start / 300 }">
{{ result }}div>
template>
<script>
import { heavy } from '@/utils'
export default {
props: ['start'],
computed: {
base () { return 42 },
result () {
const base = this.base
let result = this.start
for (let i = 0; i < 1000; i++) {
result += heavy(base)
}
return result
}
}
}
script>
这里主要是优化前后的组件的计算属性 result
的实现差异,优化前的组件多次在计算过程中访问this.base
,而优化后的组件会在计算前先用局部变量base
,缓存this.base
,后面直接访问base
。那么为啥这个差异会造成性能上的差异呢,原因是你每次访问 this.base
的时候,由于this.base
是一个响应式对象,所以会触发它的getter
,进而会执行依赖收集相关逻辑代码。类似的逻辑执行多了,像示例这样,几百次循环更新几百个组件,每个组件触发computed
重新计算,然后又多次执行依赖收集相关逻辑,性能自然就下降了。从需求上来说, this.base
执行一次依赖收集就够了,把它的getter
求值结果返回给局部变量base
,后续再次访问base
的时候就不会触发getter
,也不会走依赖收集的逻辑了,性能自然就得到了提升。引至 揭秘 Vue.js 九个性能优化技巧[4]
使用 KeepAlive
keep-alive
来缓存这个组件keep-alive
后,被 keep-alive
包裹的组件在经过第一次渲染后,的 vnode
以及 DOM 都会被缓存起来,然后再下一次再次渲染该组件的时候,直接从缓存中拿到对应的 vnode
和 DOM,然后渲染,并不需要再走一次组件初始化,render
和 patch
等一系列流程,减少了 script
的执行时间,性能更好。注意: 滥用 keep-alive 只会让你的应用变得更加卡顿,因为他会长期占用较大的内存
事件的销毁
function scrollFun(){ /* ... */}
document.addEventListener("scroll", scrollFun)
onBeforeUnmount(()=>{
document.removeEventListener("scroll", scrollFun)
})
$once
来做到这样的效果,当然你也可以在 optionsAPI
beforeDestroy 中销毁事件,但是我更加推荐前者的写法,因为后者会让相同功能的代码更分散function scrollFun(){ /* ... */}
document.addEventListener("scroll", scrollFun)
this.$once('hook:beforeDestroy', ()=>{
document.removeEventListener("scroll", scrollFun)
})
function scrollFun(){ /* ... */}
export default {
created() {
document.addEventListener("scroll", scrollFun)
},
beforeDestroy(){
document.removeEventListener("scroll", scrollFun)
}
}
图片加载
采用合理的数据处理算法
/**
* 数组转树形结构,时间复杂度O(n)
* @param list 数组
* @param idKey 元素id键
* @param parIdKey 元素父id键
* @param parId 第一级根节点的父id值
* @return {[]}
*/
function listToTree (list,idKey,parIdKey,parId) {
let map = {};
let result = [];
let len = list.length;
// 构建map
for (let i = 0; i < len; i++) {
//将数组中数据转为键值对结构 (这里的数组和obj会相互引用,这是算法实现的重点)
map[list[i][idKey]] = list[i];
}
// 构建树形数组
for(let i=0; i < len; i++) {
let itemParId = list[i][parIdKey];
// 顶级节点
if(itemParId === parId) {
result.push(list[i]);
continue;
}
// 孤儿节点,舍弃(不存在其父节点)
if(!map[itemParId]){
continue;
}
// 将当前节点插入到父节点的children中(由于是引用数据类型,obj中对于节点变化,result中对应节点会跟着变化)
if(map[itemParId].children) {
map[itemParId].children.push(list[i]);
} else {
map[itemParId].children = [list[i]];
}
}
return result;
}
其他
冻结对象(避免不需要响应式的数据变成响应式) 长列表渲染-分批渲染 长列表渲染-动态渲染(vue-virtual-scroller[5]) 微信搜索readdot,关注后回复视频教程获取23种精品资料 ...
二、 首屏/体积优化
体积 代码分割 网络
体积优化
压缩打包代码:
webpack
和vite
的生产环境打包默认就会压缩你的代码,这个一般不需要特殊处理,webpack
也可以通过对应的压缩插件手动实现取消
source-map
: 可以查看你的打包产物中是否有 .map 文件,如果有你可以将source-map
的值设置为false或者空来关闭代码映射(这个占用的体积是真的大)打包启用
gizp
压缩: 这个需要服务器也开启允许gizp
传输,不然启用了也没啥用(webpack
有对应的gzip
压缩插件,不太版本的webpack
压缩插件可能不同,建议先到官网查询)
代码分割
esModule
。所以当你使用 import()
函数来导入一个文件或者依赖,那么这个文件或者依赖就会被单独打包为一个小产物。路由懒加载
和 异步组件
都是使用这个原理。路由懒加载 异步组件
网络
外部扩展(Externals)
打包时来排除这些依赖。然后在 html 文件中通过 CDN 的方式来引入它们参考资料
https://cn.vuejs.org/v2/style-guide/#%E9%81%BF%E5%85%8D-v-if-%E5%92%8C-v-for-%E7%94%A8%E5%9C%A8%E4%B8%80%E8%B5%B7%E5%BF%85%E8%A6%81: https://link.juejin.cn?target=https%3A%2F%2Fcn.vuejs.org%2Fv2%2Fstyle-guide%2F%23%25E9%2581%25BF%25E5%2585%258D-v-if-%25E5%2592%258C-v-for-%25E7%2594%25A8%25E5%259C%25A8%25E4%25B8%2580%25E8%25B5%25B7%25E5%25BF%2585%25E8%25A6%2581
[2]https://cn.vuejs.org/v2/style-guide/#%E7%AE%80%E5%8D%95%E7%9A%84%E8%AE%A1%E7%AE%97%E5%B1%9E%E6%80%A7%E5%BC%BA%E7%83%88%E6%8E%A8%E8%8D%90: https://link.juejin.cn?target=https%3A%2F%2Fcn.vuejs.org%2Fv2%2Fstyle-guide%2F%23%25E7%25AE%2580%25E5%258D%2595%25E7%259A%2584%25E8%25AE%25A1%25E7%25AE%2597%25E5%25B1%259E%25E6%2580%25A7%25E5%25BC%25BA%25E7%2583%2588%25E6%258E%25A8%25E8%258D%2590
[3]https://slides.com/akryum/vueconfus-2019#/4/0/3: https://link.juejin.cn?target=https%3A%2F%2Fslides.com%2Fakryum%2Fvueconfus-2019%23%2F4%2F0%2F3
[4]https://juejin.cn/post/6922641008106668045: https://juejin.cn/post/6922641008106668045
[5]https://github.com/Akryum/vue-virtual-scroller: https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2FAkryum%2Fvue-virtual-scroller