Vue2 进阶实战
2026/3/20大约 9 分钟
Vue2 进阶实战:性能优化与最佳实践
经过几年的 Vue 项目开发,我踩过的坑、总结的经验都浓缩在这篇文章里。无论你是正在开发 Vue2 项目,还是在维护遗留系统,这些技巧都能帮你写出更高质量的代码。
性能优化策略
1. 合理使用 v-if 和 v-show
<!-- v-if:条件为假时不渲染 DOM -->
<!-- 适合:运行时条件很少改变的场景 -->
<HeavyComponent v-if="isReady" />
<!-- v-show:始终渲染,CSS 控制显示 -->
<!-- 适合:频繁切换的场景 -->
<TabPanel v-show="activeTab === 'panel1'" />
实际案例:
<template>
<div class="dashboard">
<!-- Tab 切换频繁,用 v-show -->
<div class="tabs">
<TabContent v-show="tab === 'overview'" />
<TabContent v-show="tab === 'analysis'" />
<TabContent v-show="tab === 'reports'" />
</div>
<!-- 弹窗不常打开,用 v-if -->
<Modal v-if="showModal" @close="showModal = false" />
<!-- 权限判断,不满足直接不渲染 -->
<AdminPanel v-if="isAdmin" />
</div>
</template>
2. v-for 优化
<!-- ❌ 避免 v-if 和 v-for 在同一元素上 -->
<li v-for="item in items" v-if="item.isActive" :key="item.id">
{{ item.name }}
</li>
<!-- ✅ 使用计算属性过滤 -->
<li v-for="item in activeItems" :key="item.id">{{ item.name }}</li>
<script>
export default {
computed: {
activeItems() {
return this.items.filter((item) => item.isActive);
},
},
};
</script>
key 的重要性:
<!-- ❌ 使用 index 作为 key(有潜在问题) -->
<li v-for="(item, index) in items" :key="index">
<!-- ✅ 使用唯一 id 作为 key -->
</li>
<li v-for="item in items" :key="item.id">
<!-- ❌ 忘记写 key -->
</li>
<li v-for="item in items"></li>
为什么 index 不好?
// 当删除中间元素时,使用 index 会导致多余的 DOM 更新
// 假设原数组 [A, B, C],key 分别是 0, 1, 2
// 删除 B 后 [A, C],key 变成 0, 1
// Vue 会认为 key=1 的元素从 B 变成了 C,触发更新
// 而实际上应该直接删除 B 的 DOM 节点
3. 组件懒加载
// 路由级别懒加载
const routes = [
{
path: "/dashboard",
component: () =>
import(
/* webpackChunkName: "dashboard" */
"./views/Dashboard.vue"
),
},
{
path: "/user",
component: () =>
import(
/* webpackChunkName: "user" */
"./views/User.vue"
),
},
];
// 组件级别懒加载
export default {
components: {
// 只有在使用时才会加载
HeavyChart: () => import("./components/HeavyChart.vue"),
// 带加载状态
AsyncModal: () => ({
component: import("./components/Modal.vue"),
loading: LoadingSpinner,
error: ErrorDisplay,
delay: 200,
timeout: 10000,
}),
},
};
4. keep-alive 缓存
<template>
<div>
<!-- 缓存动态组件 -->
<keep-alive>
<component :is="currentView" />
</keep-alive>
<!-- 缓存路由组件 -->
<keep-alive :include="['UserList', 'UserDetail']" :max="10">
<router-view />
</keep-alive>
<!-- 条件缓存 -->
<keep-alive :include="cachedViews">
<router-view :key="$route.fullPath" />
</keep-alive>
</div>
</template>
<script>
export default {
data() {
return {
cachedViews: ["Dashboard", "UserList"],
};
},
methods: {
// 动态添加缓存
addCache(name) {
if (!this.cachedViews.includes(name)) {
this.cachedViews.push(name);
}
},
// 移除缓存
removeCache(name) {
const index = this.cachedViews.indexOf(name);
if (index > -1) {
this.cachedViews.splice(index, 1);
}
},
},
};
</script>
5. 大数据列表优化
对于上千条数据的列表,直接渲染会严重影响性能:
// 方案一:分页
export default {
data() {
return {
allItems: [], // 全部数据
page: 1,
pageSize: 20
}
},
computed: {
displayItems() {
const start = (this.page - 1) * this.pageSize
return this.allItems.slice(start, start + this.pageSize)
}
}
}
// 方案二:虚拟滚动(推荐 vue-virtual-scroller)
// npm install vue-virtual-scroller
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
export default {
components: { RecycleScroller },
template: `
<RecycleScroller
:items="items"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="item">{{ item.name }}</div>
</RecycleScroller>
`
}
6. 函数式组件
对于纯展示组件,使用函数式组件可以减少开销:
<!-- 普通组件 -->
<template>
<div class="cell">
<span>{{ value }}</span>
</div>
</template>
<script>
export default {
props: ["value"],
};
</script>
<!-- 函数式组件 -->
<template functional>
<div class="cell">
<span>{{ props.value }}</span>
</div>
</template>
<!-- 或者用 render 函数 -->
<script>
export default {
functional: true,
props: ["value"],
render(h, context) {
return h("div", { class: "cell" }, [h("span", context.props.value)]);
},
};
</script>
函数式组件的特点:
- 无状态(没有 data)
- 无实例(没有 this)
- 渲染开销更低
- 适合纯展示组件
7. 冻结大型静态数据
export default {
data() {
return {
// 大型静态数据,不需要响应式
provinces: Object.freeze([
{ code: "11", name: "北京市" },
{ code: "12", name: "天津市" },
// ... 几百条数据
]),
// 需要响应式的数据正常声明
selectedProvince: null,
};
},
};
8. 事件销毁
export default {
data() {
return {
timer: null,
};
},
mounted() {
// 添加全局事件
window.addEventListener("resize", this.handleResize);
// 定时器
this.timer = setInterval(this.refresh, 5000);
// 第三方库
this.chart = echarts.init(this.$refs.chart);
},
beforeDestroy() {
// 移除全局事件
window.removeEventListener("resize", this.handleResize);
// 清除定时器
if (this.timer) {
clearInterval(this.timer);
}
// 销毁第三方库实例
if (this.chart) {
this.chart.dispose();
}
},
};
代码组织最佳实践
1. 项目结构
src/
├── api/ # API 接口
│ ├── index.js # API 统一出口
│ ├── user.js # 用户相关接口
│ └── order.js # 订单相关接口
├── assets/ # 静态资源
│ ├── images/
│ └── styles/
├── components/ # 公共组件
│ ├── base/ # 基础组件
│ │ ├── BaseButton.vue
│ │ └── BaseInput.vue
│ └── business/ # 业务组件
│ └── UserCard.vue
├── composables/ # 可复用逻辑(Vue2.7+)
├── directives/ # 自定义指令
├── filters/ # 过滤器
├── layouts/ # 布局组件
├── mixins/ # 混入
├── plugins/ # 插件
├── router/ # 路由
│ ├── index.js
│ └── modules/
├── store/ # Vuex
│ ├── index.js
│ └── modules/
├── utils/ # 工具函数
├── views/ # 页面组件
│ ├── user/
│ │ ├── UserList.vue
│ │ └── UserDetail.vue
│ └── order/
├── App.vue
└── main.js
2. API 层封装
// api/request.js - axios 封装
import axios from "axios";
import { Message } from "element-ui";
import store from "@/store";
import router from "@/router";
const service = axios.create({
baseURL: process.env.VUE_APP_API_URL,
timeout: 15000,
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
const token = store.getters.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器
service.interceptors.response.use(
(response) => {
const res = response.data;
// 根据业务状态码处理
if (res.code !== 200) {
Message.error(res.message || "请求失败");
// 特定错误码处理
if (res.code === 401) {
store.dispatch("user/logout");
router.push("/login");
}
return Promise.reject(new Error(res.message));
}
return res.data;
},
(error) => {
const status = error.response?.status;
switch (status) {
case 401:
Message.error("未授权,请重新登录");
store.dispatch("user/logout");
router.push("/login");
break;
case 403:
Message.error("拒绝访问");
break;
case 404:
Message.error("请求资源不存在");
break;
case 500:
Message.error("服务器错误");
break;
default:
Message.error(error.message || "网络错误");
}
return Promise.reject(error);
}
);
export default service;
// api/user.js - 用户接口
import request from "./request";
export function login(data) {
return request.post("/auth/login", data);
}
export function getUserInfo() {
return request.get("/user/info");
}
export function updateUser(id, data) {
return request.put(`/user/${id}`, data);
}
export function getUserList(params) {
return request.get("/user/list", { params });
}
3. Mixin 的组织
// mixins/pagination.js
export default {
data() {
return {
pagination: {
page: 1,
pageSize: 10,
total: 0
},
loading: false
}
},
computed: {
paginationProps() {
return {
currentPage: this.pagination.page,
pageSize: this.pagination.pageSize,
total: this.pagination.total,
pageSizes: [10, 20, 50, 100]
}
}
},
methods: {
handlePageChange(page) {
this.pagination.page = page
this.fetchList()
},
handleSizeChange(size) {
this.pagination.pageSize = size
this.pagination.page = 1
this.fetchList()
},
resetPagination() {
this.pagination.page = 1
}
}
}
// mixins/form.js
export default {
data() {
return {
submitting: false
}
},
methods: {
async validateAndSubmit(formRef, submitFn) {
try {
await this.$refs[formRef].validate()
this.submitting = true
await submitFn()
this.$message.success('操作成功')
} catch (error) {
if (error !== false) {
// 非表单验证错误
this.$message.error(error.message || '操作失败')
}
} finally {
this.submitting = false
}
},
resetForm(formRef) {
this.$refs[formRef]?.resetFields()
}
}
}
4. 过滤器集中管理
// filters/index.js
import Vue from "vue";
import dayjs from "dayjs";
// 日期格式化
Vue.filter("date", (value, format = "YYYY-MM-DD") => {
if (!value) return "";
return dayjs(value).format(format);
});
Vue.filter("datetime", (value) => {
if (!value) return "";
return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
});
// 金额格式化
Vue.filter("currency", (value, symbol = "¥") => {
if (value === null || value === undefined) return "";
return `${symbol}${Number(value).toFixed(2)}`;
});
// 手机号脱敏
Vue.filter("phone", (value) => {
if (!value) return "";
return value.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
});
// 状态映射
const statusMap = {
0: "待处理",
1: "处理中",
2: "已完成",
3: "已取消",
};
Vue.filter("status", (value) => statusMap[value] || "未知");
// 在 main.js 中引入
// import '@/filters'
常见问题与解决方案
1. 数据响应式问题
// 问题:添加新属性不响应
this.user.email = "test@example.com"; // 不触发更新
// 解决方案
// 方案1:$set
this.$set(this.user, "email", "test@example.com");
// 方案2:Object.assign + 替换
this.user = Object.assign({}, this.user, { email: "test@example.com" });
// 方案3:展开运算符
this.user = { ...this.user, email: "test@example.com" };
// 问题:数组索引修改不响应
this.items[0] = "new value"; // 不触发更新
// 解决方案
this.$set(this.items, 0, "new value");
// 或
this.items.splice(0, 1, "new value");
2. 组件通信问题
// 问题:深层组件通信繁琐
// 方案1:provide/inject(跨层级)
// 祖先组件
export default {
provide() {
return {
theme: this.theme,
updateTheme: this.updateTheme
}
}
}
// 后代组件
export default {
inject: ['theme', 'updateTheme']
}
// 方案2:$attrs/$listeners(透传)
// 中间组件
<template>
<ChildComponent v-bind="$attrs" v-on="$listeners" />
</template>
<script>
export default {
inheritAttrs: false
}
</script>
// 方案3:EventBus(兄弟组件)
// bus.js
import Vue from 'vue'
export default new Vue()
// 组件A
Bus.$emit('event-name', data)
// 组件B
Bus.$on('event-name', this.handler)
// 记得在 beforeDestroy 中 $off
3. 表单验证问题
<template>
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" />
</el-form-item>
<!-- 动态表单项 -->
<el-form-item
v-for="(item, index) in form.contacts"
:key="index"
:prop="'contacts.' + index + '.phone'"
:rules="contactRules.phone"
>
<el-input v-model="item.phone" />
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
// 自定义验证器
const validatePhone = (rule, value, callback) => {
if (!/^1[3-9]\d{9}$/.test(value)) {
callback(new Error("请输入正确的手机号"));
} else {
callback();
}
};
return {
form: {
username: "",
contacts: [{ phone: "" }],
},
rules: {
username: [
{ required: true, message: "请输入用户名", trigger: "blur" },
{ min: 2, max: 20, message: "长度在2到20个字符", trigger: "blur" },
],
},
contactRules: {
phone: [
{ required: true, message: "请输入手机号", trigger: "blur" },
{ validator: validatePhone, trigger: "blur" },
],
},
};
},
methods: {
async submit() {
try {
await this.$refs.formRef.validate();
// 验证通过
} catch {
// 验证失败
}
},
// 部分验证
async validateField(field) {
try {
await this.$refs.formRef.validateField(field);
return true;
} catch {
return false;
}
},
},
};
</script>
4. 路由相关问题
// 问题:路由变化但组件不更新
// 原因:组件被复用,watch $route 或使用 key
// 方案1:watch $route
watch: {
'$route'(to, from) {
this.fetchData()
}
}
// 方案2:使用 beforeRouteUpdate 钩子
beforeRouteUpdate(to, from, next) {
this.fetchData(to.params.id)
next()
}
// 方案3:给 router-view 加 key
<router-view :key="$route.fullPath" />
// 问题:组件内获取路由参数
export default {
// 方案1:通过 $route
computed: {
userId() {
return this.$route.params.id
}
},
// 方案2:通过 props(推荐)
props: ['id'] // 需要在路由配置中 props: true
}
// 路由配置
{
path: '/user/:id',
component: UserDetail,
props: true
// 或
props: route => ({ id: route.params.id, query: route.query })
}
5. 异步问题
// 问题:组件销毁后异步操作完成导致报错
export default {
data() {
return {
list: [],
cancelToken: null,
};
},
methods: {
async fetchList() {
// 方案1:检查组件是否已销毁
const res = await api.getList();
if (this._isDestroyed) return;
this.list = res;
// 方案2:使用取消令牌(axios)
this.cancelToken = axios.CancelToken.source();
try {
const res = await api.getList({
cancelToken: this.cancelToken.token,
});
this.list = res;
} catch (e) {
if (!axios.isCancel(e)) {
throw e;
}
}
},
},
beforeDestroy() {
// 取消请求
this.cancelToken?.cancel("组件销毁");
},
};
调试技巧
1. Vue Devtools
// 在 Devtools 中选中组件后
// 可以在控制台通过 $vm 访问
$vm.someData;
$vm.someMethod();
$vm.$store.state;
2. 性能分析
// 在开发环境启用性能追踪
Vue.config.performance = true;
// 在 Chrome Devtools Performance 面板可以看到:
// - Component render
// - Component patch
3. 错误处理
// 全局错误处理
Vue.config.errorHandler = (err, vm, info) => {
console.error("Vue Error:", err);
console.error("Component:", vm);
console.error("Error Info:", info);
// 上报错误到监控系统
reportError(err, { component: vm.$options.name, info });
};
// 全局警告处理(仅开发环境)
Vue.config.warnHandler = (msg, vm, trace) => {
console.warn("Vue Warning:", msg);
};
// 组件级错误边界
export default {
errorCaptured(err, vm, info) {
// 返回 false 阻止错误继续向上传播
console.error("Caught by ErrorBoundary:", err);
this.hasError = true;
return false;
},
};
单元测试
// 使用 @vue/test-utils
import { shallowMount, mount } from "@vue/test-utils";
import MyComponent from "@/components/MyComponent.vue";
describe("MyComponent", () => {
// 基础渲染测试
it("renders correctly", () => {
const wrapper = shallowMount(MyComponent, {
propsData: {
title: "Hello",
},
});
expect(wrapper.find("h1").text()).toBe("Hello");
});
// 事件测试
it("emits click event", async () => {
const wrapper = shallowMount(MyComponent);
await wrapper.find("button").trigger("click");
expect(wrapper.emitted("click")).toBeTruthy();
});
// 异步测试
it("fetches data on mount", async () => {
const wrapper = mount(MyComponent);
await wrapper.vm.$nextTick();
expect(wrapper.vm.data).not.toBeNull();
});
// Mock Vuex
it("works with vuex", () => {
const store = new Vuex.Store({
state: { user: { name: "张三" } },
});
const wrapper = shallowMount(MyComponent, { store });
expect(wrapper.find(".username").text()).toBe("张三");
});
});
总结
Vue2 虽然已经不是最新版本,但在企业级应用中仍然广泛使用。掌握这些进阶技巧,能帮助你:
- 性能优化:合理使用 v-if/v-show、keep-alive、虚拟滚动等
- 代码组织:建立清晰的项目结构,统一的 API 层和工具封装
- 问题排查:理解常见问题的原因和解决方案
- 测试调试:使用 Vue Devtools 和单元测试保证代码质量
这些经验同样适用于向 Vue3 迁移时的参考。希望本文能对你的 Vue 开发之路有所帮助。
延伸阅读
- Vue2 官方风格指南
- Vue CLI 配置参考
- Element UI 源码学习