review 别人的 Vue 代码,最常挑出来的不是什么高深问题,翻来覆去就那么几类:一串写了七八个分支的 if else、v-for 上挂着 :key="index"、一个 watch 追着另一个 watch 改数据、路由跳转里散落着写死的 path。这些写法都能跑,也都不会报错,但它们会在项目做大之后一点点变成维护负担。这篇整理九条我自己在项目里反复用到的编码技巧,每一条都讲清它解决什么问题、什么时候适用、什么时候反而不该用。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 用对象映射代替一长串
if和switch,以及它的适用边界 Array.from快速生成规律数组router.beforeEach里做跳转拦截的正确姿势,以及一个很容易写出的死循环- 用
v-if做按需渲染,它和v-show到底该怎么选 - 路由跳转为什么优先用
name而不是path v-for的key拿什么当值,用index会出什么问题computed和watch的分工- 缓存变量统一管理,以及别用
for in遍历数组
# 一、用对象映射代替 if 和 switch
循环判断然后赋值,是业务代码里出现频率极高的一类场景。一般的写法是这样:
let name = 'lisi';
let age = 18;
if (name === 'zhangsan') {
age = 21;
} else if (name === 'lisi') {
age = 18;
} else if (name === 'wangwu') {
age = 12;
}
// 或者
switch(name) {
case 'zhangsan':
age = 21;
break
case 'lisi':
age = 18;
break
case 'wangwu':
age = 12;
break
}
分支一多,这段代码就开始难看了。改成对象映射之后是这样:
let name = 'lisi';
let obj = {
zhangsan: 21,
lisi: 18,
wangwu: 12
};
let age = obj[name] || 18;
原文这里说 if 和 switch 的写法「代码执行效率不高」,这条我得改一下。对象查找是哈希查找,分支特别多的时候确实比线性比较快,但在三五个分支的规模上,这点差异现代 JS 引擎里几乎测不出来。这个技巧真正的价值不在性能,在于把「判断逻辑」变成了「数据结构」。数据结构是可以从接口拿、可以配置化、可以单独抽成文件维护的,if else 不行。
有个坑要注意,obj[name] || 18 这个兜底写法在值可能是 0 或空字符串时会失效,会被当成 falsy 然后走默认值。这种情况用 ?? 空值合并操作符更安全,只有 null 和 undefined 才走默认。
还有一点,这个技巧只适用于「判断一次然后赋一个值」的场景。如果每个分支后面还跟着一大段处理逻辑,硬塞进对象里会变成一堆函数字面量,可读性反而不如老老实实写 if。对象里存函数也不是不行,但那时候你要考虑的就是策略模式了,得配上统一的入参出参约定。
# 二、用 Array.from 快速生成数组
生成有规律的数组,比如时间选择器要用的 24 小时列表,最直觉的写法是循环 push:
let hours = [];
for (let i = 0; i < 24; i++) {
hours.push(i + '时');
}
Array.from 的第二个参数是一个映射函数,配合一个只有 length 属性的类数组对象,一行就能搞定:
let hours = Array.from({ length: 24 }, (value, index) => index + '时');
这里 { length: 24 } 是关键。Array.from 只认 length,其余索引都是 undefined,所以第一个参数 value 永远是 undefined,真正有用的是 index。写的时候一般直接用下划线占位。
这个技巧在写 mock 数据的时候特别顺手,生成 20 条假列表数据配合模板字符串,比循环 push 干净得多。
# 三、用 router.beforeEach 处理跳转前逻辑
路由跳转之前经常要做点事:设置页面标题、校验登录态、埋点、权限拦截。这些逻辑放在每个页面的 created 里会重复无数遍,正确的位置是全局前置守卫:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
// 首页
const Home = (resolve => {
require.ensure(['../views/home.vue'], () => {
resolve(require('../views/home.vue'))
})
})
let base = `${process.env.BASE_URL}`;
let router = new Router({
mode: 'history',
base: base,
routes: [
{
path: '/',
name: 'home',
component: Home,
meta: { title: '首页' }
},
]
})
router.beforeEach((to, from, next) => {
let title = to.meta && to.meta.title;
if (title) {
document.title = title; // 设置页面 title
}
next();
})
export default router
meta 字段是个好东西,页面标题、是否需要登录、需要什么权限,全塞在这里,守卫里统一读,路由表就成了一份自带元信息的配置。
原文这段守卫里还有一段拦截跳转的逻辑,写法是先调一个封装好的 $openRouter 跳到 page2,然后再调 next()。这个写法有问题,我上面把它从主流程里拿掉了,单独说。
先说注释里那个「$openRouter 方法在第 5 节中封装」,本文第五节讲的是路由用 name 不用 path,并没有这个封装,这句应该是原稿从更长的文章里摘出来时留下的残句,实际它指的是项目里自己包了一层的全局跳转方法。
更要紧的是逻辑本身。在守卫里想改变跳转目标,正确做法是把新目标交给 next,而不是自己发起一次跳转再放行原来的:
router.beforeEach((to, from, next) => {
if (to.meta && to.meta.title) {
document.title = to.meta.title;
}
// 想改跳转目标,把新目标交给 next,不要另起一次 push
if (to.name === 'home') {
next({ name: 'page2' });
return;
}
next();
})
按原文那种写法,push 会触发一次新的导航,同时 next() 又放行了当前这次导航,两次导航互相打架,运气不好就是控制台一堆 NavigationDuplicated 警告,或者直接进死循环。这个我踩过,页面卡死浏览器标签页直接没响应,排查了半天才想明白是守卫在自己递归自己。
另外 next() 必须调,而且每个分支上有且只能调一次。忘了调,页面就永远停在原地,白屏还不报错,是新手最容易懵的一类问题。
顺带更新两处写法。require.ensure 是 webpack 特有的老式异步加载 API,现在统一用标准的动态 import(),也就是 const Home = () => import('../views/home.vue'),写法短还能被 Rollup、Vite 一起理解。另外 Vue Router 4(配 Vue 3)里守卫已经不强制调 next 了,可以直接 return 一个路由地址表示重定向、return false 表示取消导航,next 仍然兼容但官方推荐用返回值的写法。
# 四、用 v-if 优化页面加载
页面上有些模块要用户主动触发才显示,弹框、抽屉、折叠面板里的复杂表单都属于这类。这种没必要一进页面就渲染,用 v-if 按需渲染就行:
<template>
<div>
<div @click="isShowModuleB = true">打开 B 模块</div>
<module-b v-if="isShowModuleB"></module-b>
</div>
</template>
<script>
import moduleB from 'components/moduleB'
export default {
data() {
return {
isShowModuleB: false
}
},
components: {
moduleB
}
}
</script>
原文这段代码有两处得改。一是模板里 div 和 module-b 是并列的两个根节点,Vue 2 的单文件组件模板必须单根,这样写编译不过,上面套了一层 div。二是点击事件里改的是 showModuleB,v-if 判断的却是 isShowModuleB,两个名字对不上,点了也不会显示,上面统一成了 isShowModuleB。
isShowModuleB 为 false 时,这个组件的实例根本不会创建,它内部的 created、mounted 都不会跑,钩子里那些耗时的接口调用自然也就省掉了。这才是 v-if 相比 v-show 真正省下来的东西,不是少了几个 DOM 节点,是少了一整套组件初始化和副作用。
那什么时候用 v-show?切换频繁的时候。v-if 每次切换都要销毁重建,v-show 只是改 display。一个 tab 用户会来回点几十次,用 v-if 就是几十次组件重建,这时候 v-show 明显更划算。
但有一条是死规矩:涉及权限的内容一律用 v-if。v-show 只是把元素藏起来,DOM 还在页面里,打开控制台改一行样式就看见了。这个跟性能无关,是安全问题。
再往前一步,如果这个模块本身体积很大,光 v-if 还不够,因为组件代码已经被打进主包了。这时候要配合异步组件,把 import moduleB from '...' 改成 components: { moduleB: () => import('components/moduleB') },让它单独拆出一个 chunk,用户不点就不下载。组件怎么拆、拆完之后彼此怎么通信,我在 Vue组件化实践详解 里系统写过。
# 五、路由跳转尽量用 name 而不是 path
前期定的路由路径,后期改动是常态。产品说 /user-center 要改成 /account,如果项目里跳转全写的 path,你得全局搜一遍挨个改,还怕漏。
name 就没这个问题。它是路由的唯一标识,跟 path 解耦,path 怎么改都不影响跳转代码:
this.$router.push({
name: 'page1'
});
// 而不是
this.$router.push({
path: 'page1'
});
顺便说一个很多人没注意的细节:用 name 跳转时传参用 params,用 path 跳转时 params 会被直接忽略,只有 query 有效。这个规则不知道的话,调试起来会很莫名其妙,参数明明传了对面就是收不到。