全栈手账 订阅 RSS
← 回到列表

Vue项目面试题

面向 Vue 项目实战的面试题集,围绕组件通信、computed 与 watch 的区别、权限控制、组件封装、接口错误处理、性能优化、页面缓存与国际化等真实场景,每题给出场景与实现思路。

读了 36 分钟 更新于 7 月 15 日

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 使用场景:

  • 根据购物车商品列表计算总价
  • 根据搜索关键词过滤列表数据
  • 拼接多个字段生成显示文本
javascript
computed: {  totalPrice() {    return this.cartList.reduce((sum, item) => sum + item.price * item.count, 0)  }}

watch 使用场景:

  • 监听路由变化重新请求数据
  • 监听搜索关键词发起防抖请求
  • 监听表单数据变化自动保存草稿
javascript
watch: {  '$route.params.id'(newId) {    this.fetchDetail(newId)  },  keyword: {    handler(val) {      this.debounceSearch(val)    },    immediate: true  }}

3. Vue 项目中如何做权限控制?使用场景有哪些?

路由权限:根据用户角色动态添加路由

javascript
// 根据后端返回的菜单动态生成路由const dynamicRoutes = filterRoutes(asyncRoutes, userRoles)router.addRoutes(dynamicRoutes)

按钮权限:通过自定义指令控制按钮显示/隐藏

javascript
// 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 遮罩
vue
<!-- 封装搜索栏组件 --><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 项目中如何处理接口请求错误?使用场景有哪些?

全局拦截器处理:

javascript
// 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,避免重复计算
vue
<!-- keep-alive 缓存列表页 --><keep-alive include="ListPage">  <router-view /></keep-alive>

7. Vue 项目中如何实现页面缓存(keep-alive)?使用场景有哪些?

基本用法:

vue
<keep-alive include="UserList,OrderList">  <router-view /></keep-alive>

结合路由 meta 配置:

javascript
// 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:

javascript
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 }})

使用方式:

vue
<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 项目中如何处理大文件上传?使用场景有哪些?

分片上传方案:

javascript
// 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 适配示例:

javascript
// 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)
javascript
// postcss.config.jsmodule.exports = {  plugins: {    'postcss-pxtorem': {      rootValue: 37.5,      propList: ['*']    }  }}

1px 边框问题:

css
/* 移动端高清屏 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等
javascript
// 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 算法核心策略:

  1. 同级比较:只比较同一层级的节点,不跨层级比较
  2. key 的作用:通过 key 快速定位可复用的节点,避免不必要的 DOM 操作
  3. 双端比较:从两端向中间比较,减少对比次数

Diff 过程:

javascript
// 简化的 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

设计组合式函数的原则:

javascript
// 组合式函数示例: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 在生命周期钩子中

javascript
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  }})

方案二:使用组合式函数封装

javascript
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 组件

vue
<Suspense>  <template #default>    <AsyncComponent />  </template>  <template #fallback>    <LoadingSpinner />  </template></Suspense>

最佳实践:

  • 请求取消:使用 AbortController 取消未完成的请求
  • 防抖节流:避免频繁触发请求
  • 错误边界:使用 onErrorCaptured 处理组件内错误
  • 缓存策略:对相同请求进行缓存,避免重复请求

15. Vue3 的编译优化有哪些?PatchFlags 是什么?

Vue3 编译优化策略:

  1. 静态提升(Static Hoisting):

    • 将静态节点提升到渲染函数外部
    • 避免每次渲染重复创建 VNode
  2. PatchFlags:

    • 编译时标记节点的更新类型
    • 运行时只更新被标记的属性
  3. Fragment:

    • 支持组件返回多个根节点
    • 减少不必要的 DOM 嵌套

PatchFlags 类型:

Flag 含义 使用场景
PROPS 属性更新 class、style、id
TEXT 文本更新 插值表达式 {{ text }}
CLASS 类名更新 :class="{ active: isActive }"
STYLE 样式更新 :style="{ color: 'red' }"
FULL_PROPS 全量属性更新 属性动态变化时
HYDRATE_EVENTS 事件绑定 @click、@input

编译前后对比:

javascript
// 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 有什么区别?

状态管理设计原则:

  1. 单一数据源:所有状态集中管理
  2. 状态只读:通过 action 修改状态
  3. 纯函数修改:mutation 应该是同步的纯函数

Pinia vs Vuex:

特性 Vuex 4 Pinia
模块化 需要 modules 每个 store 就是一个模块
命名空间 需要手动开启 天然隔离
Mutation 必须通过 mutation 修改状态 直接在 action 中修改
TypeScript 需要额外配置 原生支持
DevTools 官方支持 官方支持,更好的时间线
热更新 支持 支持

Pinia 最佳实践:

javascript
// 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 原理:

  1. 客户端请求页面
  2. 服务端渲染 Vue 组件生成 HTML
  3. 服务端返回完整 HTML 给客户端
  4. 客户端激活(hydrate)使页面可交互

实现步骤:

javascript
// 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 框架:

javascript
// 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 中如何处理内存泄漏?常见的内存泄漏场景有哪些?

常见内存泄漏场景:

  1. 未清理的事件监听器
  2. 未清理的定时器
  3. 未销毁的第三方库实例
  4. 无限增长的数组/对象引用
  5. DOM 元素引用未释放

解决方案:

javascript
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 逐层传递。

javascript
// 父组件提供依赖import { provide } from 'vue'provide('theme', 'dark')provide('user', { name: 'John' })
javascript
// 子组件注入依赖import { inject } from 'vue'const theme = inject('theme', 'light')const user = inject('user')

响应式依赖注入:

javascript
import { provide, ref } from 'vue'const theme = ref('dark')provide('theme', theme)// 修改时所有注入的组件都会响应theme.value = 'light'

多层级通信场景:

场景 实现方式
主题切换 顶层提供 theme,所有子组件注入
全局配置 提供 config 对象,子组件按需使用
权限状态 提供 user 和 permissions
国际化 提供 i18n 实例

Symbol 避免命名冲突:

javascript
// 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 中如何实现组件的异步加载和代码分割?有哪些优化策略?

路由级代码分割:

javascript
const router = createRouter({  routes: [    {      path: '/home',      component: () => import('./Home.vue')    },    {      path: '/about',      component: () => import('./About.vue')    }  ]})

组件级代码分割:

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>

带加载状态的异步组件:

javascript
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 配置示例:

javascript
module.exports = {  optimization: {    splitChunks: {      chunks: 'all',      cacheGroups: {        vendor: {          test: /[\\/]node_modules[\\/]/,          name: 'vendors',          chunks: 'all'        }      }    }  }}

前端、后端、部署、客服都是我一个人。写这里的东西都是真踩过的坑,不是抄来的教程。