Vue项目面试题
这一页
- 1. Vue 组件之间有哪些通信方式?各自的使用场景是什么?
- 2. Vue 中 computed 和 watch 的区别?各自的使用场景是什么?
- 3. Vue 项目中如何做权限控制?使用场景有哪些?
- 4. Vue 项目中如何封装一个可复用的组件?使用场景有哪些?
- 5. Vue 项目中如何处理接口请求错误?使用场景有哪些?
- 6. Vue 项目中如何做性能优化?使用场景有哪些?
- 7. Vue 项目中如何实现页面缓存(keep-alive)?使用场景有哪些?
- 8. Vue 项目中如何做国际化(i18n)?使用场景有哪些?
- 9. Vue 项目中如何处理大文件上传?使用场景有哪些?
- 10. Vue 项目中如何做移动端适配?使用场景有哪些?
- 11. Vue3 的响应式原理是什么?与 Vue2 相比有什么改进?
- 12. Vue 的虚拟 DOM 和 Diff 算法是如何工作的?
- 13. Vue3 Composition API 相比 Options API 有哪些优势?如何设计可复用的组合式函数?
- 14. Vue 中如何处理异步请求?有哪些最佳实践?
- 15. Vue3 的编译优化有哪些?PatchFlags 是什么?
- 16. Vue 中如何设计状态管理?Pinia 和 Vuex 有什么区别?
- 17. Vue 的 SSR 原理是什么?如何实现服务端渲染?
- 18. Vue 中如何处理内存泄漏?常见的内存泄漏场景有哪些?
- 19. Vue 的依赖注入(provide/inject)原理是什么?如何实现多层级通信?
- 20. Vue 中如何实现组件的异步加载和代码分割?有哪些优化策略?
Vue项目面试题
面向 Vue 项目实战的面试题集,围绕组件通信、computed 与 watch 的区别、权限控制、组件封装、接口错误处理、性能优化、页面缓存与国际化等真实场景,每题给出场景与实现思路。
1. Vue 组件之间有哪些通信方式?各自的使用场景是什么?
| 通信方式 | 使用场景 |
|---|---|
| props / $emit | 父子组件通信,父传子用 props,子传父用 $emit 触发事件 |
| v-model | 父子组件双向绑定,如表单组件封装、开关组件 |
| provide / inject | 祖孙组件跨层级通信,如主题配置、全局状态注入 |
| EventBus($on / $emit) | 兄弟组件或任意组件通信,如非父子关系的两个组件需要联动 |
| Vuex / Pinia | 大型项目全局状态管理,如用户信息、购物车、权限等跨多组件共享的数据 |
| $refs | 父组件直接调用子组件方法,如父组件需要触发子组件的表单校验 |
| $parent / $children | 访问父/子组件实例,如弹窗组件内部访问父组件数据(不推荐频繁使用) |
| $attrs / $listeners | 二次封装第三方组件时透传属性和事件,如封装 Element Plus 的 Input 组件 |
2. Vue 中 computed 和 watch 的区别?各自的使用场景是什么?
| 特性 | computed | watch |
|---|---|---|
| 缓存 | 有缓存,依赖不变则不重新计算 | 无缓存,每次数据变化都执行 |
| 返回值 | 必须有返回值 | 不需要返回值 |
| 副作用 | 不应有副作用(如异步请求) | 可以有副作用 |
computed 使用场景:
- 根据购物车商品列表计算总价
- 根据搜索关键词过滤列表数据
- 拼接多个字段生成显示文本
computed: { totalPrice() { return this.cartList.reduce((sum, item) => sum + item.price * item.count, 0) }}watch 使用场景:
- 监听路由变化重新请求数据
- 监听搜索关键词发起防抖请求
- 监听表单数据变化自动保存草稿
watch: { '$route.params.id'(newId) { this.fetchDetail(newId) }, keyword: { handler(val) { this.debounceSearch(val) }, immediate: true }}3. Vue 项目中如何做权限控制?使用场景有哪些?
路由权限:根据用户角色动态添加路由
// 根据后端返回的菜单动态生成路由const dynamicRoutes = filterRoutes(asyncRoutes, userRoles)router.addRoutes(dynamicRoutes)按钮权限:通过自定义指令控制按钮显示/隐藏
// v-permission 指令app.directive('permission', { mounted(el, binding) { const userPermissions = store.state.permissions if (!userPermissions.includes(binding.value)) { el.parentNode?.removeChild(el) } }})// 使用<el-button v-permission="'user:delete'">删除</el-button>使用场景:
- 后台管理系统中,不同角色(管理员/普通用户)看到不同菜单
- 同一页面中,有编辑权限的人才能看到"编辑"按钮
- 数据级别的权限,如只能查看自己部门的数据
4. Vue 项目中如何封装一个可复用的组件?使用场景有哪些?
封装原则:
- 通过 props 接收配置,通过 emit 暴露事件
- 提供 slot 支持内容分发
- 提供合理的默认值
使用场景:
| 组件类型 | 场景 | 示例 |
|---|---|---|
| 表单组件 | 多个页面都有相同的表单 | 用户信息表单、搜索筛选栏 |
| 表格组件 | 多个页面都有 CRUD 表格 | 数据列表页,支持分页、搜索、排序 |
| 弹窗组件 | 多处需要确认操作 | 删除确认、表单编辑弹窗 |
| 空状态组件 | 列表无数据时展示 | "暂无数据"、"网络异常" |
| 加载组件 | 数据请求时展示 | 骨架屏、loading 遮罩 |
<!-- 封装搜索栏组件 --><template> <el-form :model="form" inline> <slot :form="form" /> <el-button type="primary" @click="$emit('search', form)">搜索</el-button> <el-button @click="handleReset">重置</el-button> </el-form></template>5. Vue 项目中如何处理接口请求错误?使用场景有哪些?
全局拦截器处理:
// axios 响应拦截器service.interceptors.response.use( response => { const { code, data, message } = response.data if (code === 0) return data if (code === 401) { // token 过期,跳转登录 router.push('/login') return Promise.reject(new Error('登录已过期')) } ElMessage.error(message || '请求失败') return Promise.reject(new Error(message)) }, error => { if (error.response?.status === 500) { ElMessage.error('服务器异常,请稍后重试') } else if (!window.navigator.onLine) { ElMessage.error('网络连接已断开') } return Promise.reject(error) })使用场景:
- 401 未授权 → 自动跳转登录页
- 403 无权限 → 提示无权限并停留在当前页
- 500 服务器错误 → 提示服务器异常
- 网络断开 → 提示网络异常
- 业务错误码 → 根据错误码展示对应提示
6. Vue 项目中如何做性能优化?使用场景有哪些?
| 优化手段 | 使用场景 |
|---|---|
| 路由懒加载 | 首屏加载优化,按需加载页面组件:const Detail = () => import('./Detail.vue') |
| v-show vs v-if | 频繁切换用 v-show(如 Tab 切换),条件基本不变用 v-if(如权限控制显示) |
| keep-alive 缓存 | 列表页跳详情页再返回时保留列表滚动位置和已加载数据 |
| 虚拟滚动 | 长列表渲染(如聊天记录、商品列表超过 1000 条) |
| 防抖/节流 | 搜索输入框防抖、滚动事件节流、按钮防重复点击 |
| 图片懒加载 | 商品列表页图片较多时,进入视口再加载 |
| 组件按需引入 | Element Plus 等UI库按需引入,减小打包体积 |
| computed 缓存 | 复杂计算逻辑用 computed 代替 methods,避免重复计算 |
<!-- keep-alive 缓存列表页 --><keep-alive include="ListPage"> <router-view /></keep-alive>7. Vue 项目中如何实现页面缓存(keep-alive)?使用场景有哪些?
基本用法:
<keep-alive include="UserList,OrderList"> <router-view /></keep-alive>结合路由 meta 配置:
// router.js{ path: '/list', component: () => import('@/views/List.vue'), meta: { keepAlive: true }}// App.vue<keep-alive> <router-view v-if="$route.meta.keepAlive" /></keep-alive><router-view v-if="!$route.meta.keepAlive" />使用场景:
- 列表页 → 详情页 → 返回列表页,保留搜索条件和滚动位置
- 多 Tab 页切换,切换回来时保留之前的操作状态
- 表单填写页跳转其他页面再返回,保留已填写的数据
注意事项:
- 缓存的组件会触发
activated/deactivated而非mounted/unmounted - 需要在
activated中刷新数据(如列表页返回后需要刷新最新数据)
8. Vue 项目中如何做国际化(i18n)?使用场景有哪些?
配置 vue-i18n:
import { createI18n } from 'vue-i18n'import zh from './locales/zh'import en from './locales/en'const i18n = createI18n({ legacy: false, locale: localStorage.getItem('lang') || 'zh', messages: { zh, en }})使用方式:
<template> <p>{{ t('common.confirm') }}</p> <el-button @click="switchLang">切换语言</el-button></template><script setup>import { useI18n } from 'vue-i18n'const { t, locale } = useI18n()const switchLang = () => { locale.value = locale.value === 'zh' ? 'en' : 'zh' localStorage.setItem('lang', locale.value)}</script>使用场景:
- 外贸网站需要支持中英文切换
- 后台管理系统需要支持多语言
- 日期、货币格式根据地区显示不同格式
9. Vue 项目中如何处理大文件上传?使用场景有哪些?
分片上传方案:
// 1. 文件分片function createFileChunk(file, chunkSize = 5 * 1024 * 1024) { const chunks = [] for (let i = 0; i < file.size; i += chunkSize) { chunks.push(file.slice(i, i + chunkSize)) } return chunks}// 2. 计算文件 hash(用于秒传判断)async function calculateHash(chunks) { const spark = new SparkMD5.ArrayBuffer() for (const chunk of chunks) { spark.append(await chunk.arrayBuffer()) } return spark.end()}// 3. 上传分片async function uploadChunks(file, hash, chunks) { // 先检查是否已上传过(秒传) const { uploadedChunks } = await checkFile(hash) // 只上传未上传的分片(断点续传) const tasks = chunks .filter((_, i) => !uploadedChunks.includes(i)) .map((chunk, i) => uploadChunk({ chunk, hash, index: i })) await Promise.all(tasks) // 通知后端合并分片 await mergeFile({ hash, filename: file.name, size: chunks.length })}使用场景:
- 视频上传:课程平台上传教学视频,文件通常几百MB到几GB
- 文档上传:企业OA系统上传大型设计文件或数据包
- 批量导入:ERP系统导入大批量数据的Excel文件
核心能力:
- 分片上传:将大文件切分为小片逐个上传
- 断点续传:上传中断后从已上传的分片继续
- 秒传:通过文件hash判断服务端是否已存在相同文件
10. Vue 项目中如何做移动端适配?使用场景有哪些?
| 适配方案 | 使用场景 | 实现方式 |
|---|---|---|
| rem 适配 | H5页面需要等比缩放 | postcss-pxtorem + amfe-flexible,设计稿按 375px 开发 |
| vw/vh 适配 | 现代浏览器H5页面 | postcss-px-to-viewport,直接用视口单位 |
| 响应式布局 | 需要同时适配PC和移动端 | 媒体查询 @media + 弹性布局 |
| viewport 缩放 | 大屏数据可视化 | 根据屏幕尺寸动态计算 scale 值 |
rem 适配示例:
// amfe-flexible 动态设置 root font-size// 设计稿 375px,1rem = 37.5px;(function(win, doc) { const docEl = doc.documentElement function setRemUnit() { docEl.style.fontSize = docEl.clientWidth / 10 + 'px' } setRemUnit() win.addEventListener('resize', setRemUnit)})(window, document)// postcss.config.jsmodule.exports = { plugins: { 'postcss-pxtorem': { rootValue: 37.5, propList: ['*'] } }}1px 边框问题:
/* 移动端高清屏 1px 边框变粗 */.border-1px::after { content: ''; position: absolute; left: 0; bottom: 0; width: 100%; height: 1px; background: #eee; transform: scaleY(0.5); transform-origin: 0 0;}使用场景:
- 电商H5活动页需要在各种手机上等比展示
- 企业官网需要同时适配PC和移动端
- 大屏数据看板需要根据屏幕尺寸自适应缩放
- 微信公众号H5页面需要兼容不同机型
11. Vue3 的响应式原理是什么?与 Vue2 相比有什么改进?
Vue2 响应式原理:使用 Object.defineProperty 遍历对象的每个属性,为其添加 getter/setter 来拦截属性的访问和修改。
Vue3 响应式原理:使用 ES6 的 Proxy 代理整个对象,可以直接监听对象属性的新增和删除,以及数组的变化。
| 特性 | Vue2 Object.defineProperty | Vue3 Proxy |
|---|---|---|
| 监听新增属性 | 不支持,需要 Vue.set() |
原生支持 |
| 监听删除属性 | 不支持,需要 Vue.delete() |
原生支持 |
| 监听数组索引/长度 | 不支持 | 原生支持 |
| 性能 | 初始化时递归遍历所有属性 | 懒代理,访问时才触发 |
| 类型支持 | 仅支持对象/数组 | 支持对象、数组、Map、Set等 |
// Vue3 reactive 核心实现function createReactiveObject(target) { const proxy = new Proxy(target, { get(target, key, receiver) { const result = Reflect.get(target, key, receiver) track(target, key) return isObject(result) ? reactive(result) : result }, set(target, key, value, receiver) { const oldValue = target[key] const result = Reflect.set(target, key, value, receiver) if (oldValue !== value) { trigger(target, key) } return result }, deleteProperty(target, key) { const hadKey = hasOwn(target, key) const result = Reflect.deleteProperty(target, key) if (hadKey) { trigger(target, key) } return result } }) return proxy}12. Vue 的虚拟 DOM 和 Diff 算法是如何工作的?
虚拟 DOM:用 JavaScript 对象描述 DOM 结构,通过 diff 算法计算出最小更新路径,再批量操作真实 DOM。
Diff 算法核心策略:
- 同级比较:只比较同一层级的节点,不跨层级比较
- key 的作用:通过 key 快速定位可复用的节点,避免不必要的 DOM 操作
- 双端比较:从两端向中间比较,减少对比次数
Diff 过程:
// 简化的 diff 过程function patch(oldVnode, newVnode) { if (sameVnode(oldVnode, newVnode)) { patchVnode(oldVnode, newVnode) } else { const oldEl = oldVnode.el const parentEl = api.parentNode(oldEl) createElm(newVnode, parentEl) api.removeChild(parentEl, oldEl) }}function patchVnode(oldVnode, newVnode) { const el = newVnode.el = oldVnode.el const oldCh = oldVnode.children const newCh = newVnode.children if (oldCh && newCh && oldCh !== newCh) { updateChildren(el, oldCh, newCh) } else if (newCh) { addVnodes(el, null, newCh, 0, newCh.length - 1) } else if (oldCh) { removeVnodes(el, oldCh, 0, oldCh.length - 1) }}Vue3 的优化:
- 静态提升:将静态节点提升到渲染函数外部,避免每次渲染重复创建
- PatchFlags:编译时标记节点的更新类型,运行时只更新必要的属性
13. Vue3 Composition API 相比 Options API 有哪些优势?如何设计可复用的组合式函数?
Composition API 的优势:
| 维度 | Options API | Composition API |
|---|---|---|
| 代码组织 | 按选项分组,逻辑分散 | 按功能分组,逻辑内聚 |
| 逻辑复用 | 依赖 mixins,存在命名冲突 | 组合式函数,清晰的依赖关系 |
| 类型推断 | 需要额外的类型声明 | 原生支持 TypeScript |
| 响应式 | 依赖 this | 直接使用 ref/reactive |
设计组合式函数的原则:
// 组合式函数示例:useUserimport { ref, onMounted, onUnmounted } from 'vue'export function useUser() { const user = ref(null) const loading = ref(false) const error = ref(null) const fetchUser = async (userId) => { loading.value = true error.value = null try { user.value = await api.getUser(userId) } catch (e) { error.value = e.message } finally { loading.value = false } } return { user, loading, error, fetchUser }}// 使用const { user, loading, fetchUser } = useUser()设计原则:
- 命名规范:以
use开头,如useUser、useFetch - 返回响应式状态:使用
ref/reactive返回状态 - 提供操作方法:返回修改状态的函数
- 内部管理副作用:在函数内部处理生命周期
14. Vue 中如何处理异步请求?有哪些最佳实践?
方案一:async/await 在生命周期钩子中
import { ref, onMounted } from 'vue'const data = ref(null)const loading = ref(true)onMounted(async () => { try { data.value = await fetchData() } catch (e) { console.error('加载失败:', e) } finally { loading.value = false }})方案二:使用组合式函数封装
export function useAsyncData(fn) { const data = ref(null) const loading = ref(true) const error = ref(null) const execute = async () => { loading.value = true error.value = null try { data.value = await fn() } catch (e) { error.value = e } finally { loading.value = false } } execute() return { data, loading, error, execute }}方案三:使用 Suspense 组件
<Suspense> <template #default> <AsyncComponent /> </template> <template #fallback> <LoadingSpinner /> </template></Suspense>最佳实践:
- 请求取消:使用
AbortController取消未完成的请求 - 防抖节流:避免频繁触发请求
- 错误边界:使用
onErrorCaptured处理组件内错误 - 缓存策略:对相同请求进行缓存,避免重复请求
15. Vue3 的编译优化有哪些?PatchFlags 是什么?
Vue3 编译优化策略:
静态提升(Static Hoisting):
- 将静态节点提升到渲染函数外部
- 避免每次渲染重复创建 VNode
PatchFlags:
- 编译时标记节点的更新类型
- 运行时只更新被标记的属性
Fragment:
- 支持组件返回多个根节点
- 减少不必要的 DOM 嵌套
PatchFlags 类型:
| Flag | 含义 | 使用场景 |
|---|---|---|
PROPS |
属性更新 | class、style、id |
TEXT |
文本更新 | 插值表达式 {{ text }} |
CLASS |
类名更新 | :class="{ active: isActive }" |
STYLE |
样式更新 | :style="{ color: 'red' }" |
FULL_PROPS |
全量属性更新 | 属性动态变化时 |
HYDRATE_EVENTS |
事件绑定 | @click、@input |
编译前后对比:
// Vue2 编译结果:每次渲染都创建新的 VNoderender() { return _createVNode('div', { class: 'container' }, [ _createVNode('span', null, '静态文本'), _createVNode('span', null, this.text) ])}// Vue3 编译结果:静态节点被提升,动态节点标记 PatchFlagconst _hoisted_1 = _createVNode('span', null, '静态文本', 1 /* TEXT */)render() { return _createVNode('div', { class: 'container' }, [ _hoisted_1, _createVNode('span', null, this.text, 1 /* TEXT */) ])}16. Vue 中如何设计状态管理?Pinia 和 Vuex 有什么区别?
状态管理设计原则:
- 单一数据源:所有状态集中管理
- 状态只读:通过 action 修改状态
- 纯函数修改:mutation 应该是同步的纯函数
Pinia vs Vuex:
| 特性 | Vuex 4 | Pinia |
|---|---|---|
| 模块化 | 需要 modules | 每个 store 就是一个模块 |
| 命名空间 | 需要手动开启 | 天然隔离 |
| Mutation | 必须通过 mutation 修改状态 | 直接在 action 中修改 |
| TypeScript | 需要额外配置 | 原生支持 |
| DevTools | 官方支持 | 官方支持,更好的时间线 |
| 热更新 | 支持 | 支持 |
Pinia 最佳实践:
// stores/user.jsimport { defineStore } from 'pinia'import { ref, computed } from 'vue'export const useUserStore = defineStore('user', () => { const user = ref(null) const token = ref(localStorage.getItem('token')) const isLoggedIn = computed(() => !!token.value) const login = async (credentials) => { const { data } = await api.login(credentials) user.value = data.user token.value = data.token localStorage.setItem('token', data.token) } const logout = () => { user.value = null token.value = null localStorage.removeItem('token') } return { user, token, isLoggedIn, login, logout }})17. Vue 的 SSR 原理是什么?如何实现服务端渲染?
SSR 原理:
- 客户端请求页面
- 服务端渲染 Vue 组件生成 HTML
- 服务端返回完整 HTML 给客户端
- 客户端激活(hydrate)使页面可交互
实现步骤:
// server.jsconst { createSSRApp } = require('vue')const { renderToString } = require('@vue/server-renderer')const express = require('express')const app = express()app.get('*', async (req, res) => { const app = createSSRApp({ template: `<div>Hello SSR</div>` }) const html = await renderToString(app) res.send(` <!DOCTYPE html> <html> <head> <title>SSR</title> </head> <body> ${html} <script src="/client-bundle.js"></script> </body> </html> `)})Nuxt.js 框架:
// nuxt.config.jsexport default { ssr: true, target: 'server'}// pages/index.vueexport default { async asyncData({ $axios }) { const posts = await $axios.$get('/api/posts') return { posts } }}SSR 适用场景:
- 需要 SEO 优化的页面
- 首屏加载性能要求高
- 需要良好的社交媒体分享体验
SSR 注意事项:
- 避免在组件中使用浏览器特定的 API(如
window、document) - 使用
onServerPrefetch处理服务端数据预取 - 注意内存泄漏,每次请求创建新的应用实例
18. Vue 中如何处理内存泄漏?常见的内存泄漏场景有哪些?
常见内存泄漏场景:
- 未清理的事件监听器
- 未清理的定时器
- 未销毁的第三方库实例
- 无限增长的数组/对象引用
- DOM 元素引用未释放
解决方案:
import { onUnmounted } from 'vue'export function useEventListener(target, event, handler) { onMounted(() => { target.addEventListener(event, handler) }) onUnmounted(() => { target.removeEventListener(event, handler) })}export function useInterval(callback, delay) { const intervalId = ref(null) onMounted(() => { intervalId.value = setInterval(callback, delay) }) onUnmounted(() => { if (intervalId.value) { clearInterval(intervalId.value) } })}检测内存泄漏:
- 使用 Chrome DevTools 的 Memory 面板
- 记录堆快照,对比前后变化
- 使用
performance.mark()和performance.measure()分析
19. Vue 的依赖注入(provide/inject)原理是什么?如何实现多层级通信?
原理:
provide 在父组件中注册依赖,inject 在任意子组件中获取依赖,无需通过 props 逐层传递。
// 父组件提供依赖import { provide } from 'vue'provide('theme', 'dark')provide('user', { name: 'John' })// 子组件注入依赖import { inject } from 'vue'const theme = inject('theme', 'light')const user = inject('user')响应式依赖注入:
import { provide, ref } from 'vue'const theme = ref('dark')provide('theme', theme)// 修改时所有注入的组件都会响应theme.value = 'light'多层级通信场景:
| 场景 | 实现方式 |
|---|---|
| 主题切换 | 顶层提供 theme,所有子组件注入 |
| 全局配置 | 提供 config 对象,子组件按需使用 |
| 权限状态 | 提供 user 和 permissions |
| 国际化 | 提供 i18n 实例 |
Symbol 避免命名冲突:
// keys.jsexport const THEME_KEY = Symbol('theme')export const USER_KEY = Symbol('user')// 父组件import { provide } from 'vue'import { THEME_KEY } from './keys'provide(THEME_KEY, 'dark')// 子组件import { inject } from 'vue'import { THEME_KEY } from './keys'const theme = inject(THEME_KEY)20. Vue 中如何实现组件的异步加载和代码分割?有哪些优化策略?
路由级代码分割:
const router = createRouter({ routes: [ { path: '/home', component: () => import('./Home.vue') }, { path: '/about', component: () => import('./About.vue') } ]})组件级代码分割:
<template> <div> <AsyncComponent v-if="show" /> </div></template><script setup>import { defineAsyncComponent, ref } from 'vue'const show = ref(false)const AsyncComponent = defineAsyncComponent(() => import('./HeavyComponent.vue'))</script>带加载状态的异步组件:
const AsyncComponent = defineAsyncComponent({ loader: () => import('./HeavyComponent.vue'), loadingComponent: LoadingComponent, errorComponent: ErrorComponent, delay: 200, timeout: 3000})优化策略:
| 策略 | 实现方式 | 效果 |
|---|---|---|
| 预加载 | import(/* webpackPrefetch: true */ './Component.vue') |
空闲时提前加载 |
| 预获取 | import(/* webpackPreload: true */ './Component.vue') |
当前页面立即加载 |
| 分块策略 | webpack 的 splitChunks 配置 | 公共依赖提取 |
| 动态导入 | 根据条件按需加载 | 减少初始包体积 |
Webpack 配置示例:
module.exports = { optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all' } } } }}