Vue 前端开发 - 面试题库
一、基础题 ⭐
Q1. Vue 3 的响应式系统是如何工作的?
答案:Vue 3 使用 Proxy 对象实现响应式。reactive() 将对象包装为 Proxy,拦截 get 和 set 操作。在 get 时通过 track() 收集依赖(记录哪些 effect 访问了该属性),在 set 时通过 trigger() 触发依赖更新。相比 Vue 2 的 Object.defineProperty,Proxy 可以拦截属性新增、删除、数组索引访问等操作,无需递归遍历所有属性,性能更好且支持 Map、Set 等集合类型。
关联知识点:Proxy、响应式原理、依赖收集、Vue 3
Q2. Vue 组件的生命周期有哪些?各阶段适合做什么?
答案:Vue 3 组合式 API 中主要生命周期钩子:onBeforeMount(DOM 挂载前)→ onMounted(DOM 已挂载,适合发起请求、操作 DOM、注册事件监听)→ onBeforeUpdate(数据变化但 DOM 未更新前)→ onUpdated(DOM 已更新,避免在此修改状态以防无限循环)→ onBeforeUnmount(组件销毁前,适合清理定时器、取消订阅)→ onUnmounted(组件已销毁)。setup() 本身在 beforeCreate 和 created 之间执行,替代了这两个钩子。
关联知识点:生命周期、组合式 API、DOM 操作
Q3. v-if 和 v-show 的区别是什么?如何选择?
答案:v-if 是真正的条件渲染,条件为假时组件不会渲染到 DOM 中,切换时会销毁和重建组件,触发完整的生命周期。v-show 始终渲染组件,仅通过 CSS display: none 控制显隐,切换开销小。选择原则:频繁切换的场景用 v-show(如 Tab 切换、弹窗),运行时条件很少改变或初始为假的场景用 v-if(如权限控制、懒加载组件)。v-if 支持 <template> 分组,v-show 不支持。
关联知识点:条件渲染、性能优化、DOM 操作
Q4. Vue 中 ref 和 reactive 的区别是什么?
答案:ref 用于创建基本类型或对象的响应式引用,通过 .value 访问值,在模板中自动解包无需 .value。reactive 仅适用于对象类型(对象、数组、Map、Set),返回 Proxy 代理对象,无需 .value。ref 内部对于对象类型也会调用 reactive 进行深层转换。选择建议:基本类型必须用 ref;对象类型两者皆可,但 ref 更灵活(可随时替换整个对象);reactive 解构会丢失响应式,需用 toRefs 转换。
关联知识点:响应式 API、组合式 API、响应式解构
Q5. Vue 组件之间有哪些通信方式?
答案:父子通信:父通过 props 传递,子通过 emit 触发事件通知父。兄弟通信:通过共同父组件中转或使用 provide/inject。跨层级通信:provide/inject 可在祖先和后代之间传递数据,无需逐层透传。全局通信:使用 Pinia 或 Vuex 状态管理。其他:$parent/$children(不推荐,耦合度高)、$refs(父访问子实例)、事件总线(Vue 3 已移除 EventEmitter,可用 mitt 等第三方库)。
关联知识点:组件通信、props、emit、provide/inject
Q6. 什么是计算属性(computed)和侦听器(watch)?何时使用?
答案:computed 是基于响应式依赖缓存的派生状态,只有依赖变化时才重新计算,适合从现有数据派生新数据(如过滤列表、格式化显示)。watch 用于在数据变化时执行副作用(如发起请求、操作 DOM),支持 immediate 立即执行和 deep 深度监听。选择原则:派生数据用 computed,执行异步操作或开销大的操作用 watch。computed 默认只读,可定义 get 和 set 实现可写计算属性。
关联知识点:计算属性、侦听器、缓存机制
Q7. Vue 中 key 属性的作用是什么?
答案:key 是 VNode 的唯一标识,帮助 Vue 的 diff 算法高效识别和复用节点。使用 key 可以让 Vue 精确追踪每个元素的身份,避免就地复用导致的错误(如列表中包含输入框时状态混乱)。最佳实践:使用稳定、唯一的 ID 作为 key(如数据库 ID),避免使用数组索引(会导致不必要的重渲染和状态丢失)。在 <transition-group> 和组件切换时,key 变化会触发组件的销毁和重建。
关联知识点:diff 算法、虚拟 DOM、列表渲染
Q8. Vue 插槽(slot)的作用是什么?有哪些类型?
答案:插槽允许父组件向子组件插入内容,实现内容分发和组件组合。三种类型:默认插槽(<slot> 无 name 属性)、具名插槽(<slot name="header"> 配合 <template #header> 使用)、作用域插槽(<slot :item="item"> 子组件向父组件传递数据,父组件通过 v-slot="{ item }" 接收)。作用域插槽常用于列表组件、表格组件,让父组件自定义每行渲染逻辑,提高组件复用性。
关联知识点:插槽、内容分发、作用域插槽、组件复用
Q9. Vue 中 nextTick 的作用是什么?
答案:nextTick 用于在 DOM 更新完成后执行回调。Vue 的 DOM 更新是异步的,数据变化后不会立即更新 DOM,而是将更新放入微任务队列批量执行。当需要在数据变化后立即操作更新后的 DOM 时(如获取元素尺寸、聚焦输入框),必须使用 nextTick 等待 DOM 更新完成。Vue 3 中通过 import { nextTick } from 'vue' 引入,也可用 await nextTick() 写法。
关联知识点:异步更新、DOM 操作、微任务
Q10. Vue 指令(directive)是什么?如何自定义指令?
答案:指令是带有 v- 前缀的特殊属性,用于对 DOM 进行底层操作(如 v-bind、v-on、v-model)。自定义指令通过 app.directive('focus', { mounted(el) { el.focus() } }) 注册,生命周期钩子包括 created(绑定元素属性或事件监听器设置前)、mounted(元素插入父节点时)、beforeUpdate(更新前)、updated、beforeUnmount、unmounted。适用场景:自动聚焦、图片懒加载、权限按钮控制、拖拽等需要直接操作 DOM 的场景。
关联知识点:自定义指令、DOM 操作、生命周期
二、进阶题 ⭐⭐
Q11. Vue 3 的 Composition API 相比 Options API 有什么优势?
答案:Composition API 解决了 Options API 的三个核心问题:1)逻辑复用:Options API 按 data/methods/computed 组织代码,相关逻辑分散;Composition API 按功能组织,可将相关逻辑提取为 composable 函数(如 useUser()),通过 hooks 模式复用。2)TypeScript 支持:Options API 中 this 类型推断困难,Composition API 中函数式写法天然支持 TS 类型推导。3)代码组织:大型组件中逻辑更清晰,相关代码聚集在一起。Composition API 完全兼容 Options API,可渐进式使用。
关联知识点:组合式 API、逻辑复用、TypeScript、代码组织
Q12. Vue Router 的导航守卫有哪些?执行顺序是怎样的?
答案:全局守卫:beforeEach(全局前置)、beforeResolve(解析前)、afterEach(后置,不接收 next)。路由独享守卫:beforeEnter(定义在路由配置中)。组件内守卫:beforeRouteEnter(进入前,不能访问 this)、beforeRouteUpdate(路由复用组件时)、beforeRouteLeave(离开前)。完整导航流程:beforeRouteLeave → beforeEach → beforeRouteUpdate(复用场景)→ beforeEnter → beforeRouteEnter → beforeResolve → afterEach。守卫中调用 next(false) 取消导航,next('/path') 重定向,next(error) 中断并传递错误。
关联知识点:导航守卫、路由、权限控制
Q13. Vue 中如何实现路由懒加载?原理是什么?
答案:路由懒加载通过动态 import() 语法实现:const Home = () => import('./views/Home.vue')。Webpack 或 Vite 会将该组件打包为独立的 chunk,在路由首次访问时才加载该 chunk,减少初始包体积。配合 Vue Router 使用时,可结合 loadingComponent 和 delay 配置加载状态。对于大型应用,可按路由模块拆分 chunk:import(/* webpackChunkName: "user" */ './views/User.vue')。懒加载的缺点是首次访问该路由时会有加载延迟,可通过预加载策略优化。
关联知识点:代码分割、动态 import、性能优化、Webpack
Q14. Vuex 和 Pinia 有什么区别?为什么 Vue 3 推荐 Pinia?
答案:Pinia 是 Vue 官方推荐的状态管理库,相比 Vuex 的优势:1)更简洁的 API:去除了 mutations,只有 state、getters、actions,减少样板代码;2)更好的 TypeScript 支持:完整的类型推断,无需额外配置;3)模块化设计:每个 store 是独立的,按需导入,支持热更新;4)体积更小:约 1KB,支持 Tree-shaking;5)支持组合式 API:可直接在 setup 中使用。Vuex 4 兼容 Vue 3 但进入维护模式,新项目应优先选择 Pinia。 关联知识点:状态管理、Pinia、Vuex、TypeScript
Q15. Vue 中 Keep-alive 的作用是什么?有哪些属性?
答案:<Keep-alive> 是内置组件,用于缓存动态组件或路由组件的实例,避免重复渲染和状态丢失。包裹的组件在切换时不会销毁,而是将 DOM 和实例缓存起来,再次激活时恢复状态。属性:include(字符串或正则,匹配 name 的组件被缓存)、exclude(匹配的组件不缓存)、max(最大缓存数量,超出时按 LRU 策略淘汰)。配合路由使用时,被缓存的组件触发 activated 和 deactivated 钩子,而非 mounted/unmounted。
关联知识点:组件缓存、LRU 算法、生命周期
Q16. Vue 中如何实现组件的异步加载?
答案:使用 defineAsyncComponent 定义异步组件:const AsyncComp = defineAsyncComponent(() => import('./MyComponent.vue'))。支持配置项:loadingComponent(加载中显示的组件)、errorComponent(加载失败时显示)、delay(延迟显示 loading,默认 200ms)、timeout(超时时间)、retry(重试次数,需配合第三方库)。适用场景:大型组件(如富文本编辑器、图表)、弹窗内容、路由级代码分割。异步组件在 Suspense 中可与 await 配合使用,实现更优雅的加载状态管理。
关联知识点:异步组件、代码分割、Suspense、性能优化
Q17. Vue 中 v-model 的原理是什么?如何在自定义组件上使用?
答案:v-model 是语法糖,在原生元素上等价于 :value + @input,在自定义组件上等价于 :modelValue + @update:modelValue(Vue 3)。自定义组件实现 v-model:props 中声明 modelValue,用户输入时通过 emit('update:modelValue', newValue) 通知父组件更新。支持多个 v-model:v-model:title 对应 title + update:title。修饰符如 .lazy、.number、.trim 在自定义组件中需在组件内部处理。
关联知识点:双向绑定、语法糖、自定义组件事件
Q18. Vue 中的 Teleport(传送门)是什么?使用场景有哪些?
答案:<Teleport> 是 Vue 3 内置组件,将组件的 DOM 渲染到指定的 DOM 节点下,而非组件树中的位置。语法:<Teleport to="body"> 或 to="#modal-root"。解决 CSS 层级问题:模态框、通知、下拉菜单等组件如果嵌套在 overflow: hidden 或 z-index 受限的容器中,会导致显示异常。通过 Teleport 将 DOM 移到 body 下,可避免这些问题,同时保持组件的逻辑父子关系(props、emit 正常工作)。
关联知识点:DOM 渲染、模态框、CSS 层级
三、高级题 ⭐⭐⭐
Q19. Vue 的虚拟 DOM 和 diff 算法是如何工作的?
答案:虚拟 DOM 是用 JavaScript 对象描述真实 DOM 的树结构(VNode)。当数据变化时,Vue 生成新的 VNode 树,与旧 VNode 树进行 diff 比较,找出最小变更并应用到真实 DOM。Vue 3 的 diff 算法优化:1)静态标记(Patch Flags):编译时标记动态节点,diff 时跳过静态节点;2)静态提升(Hoisting):静态节点提升到渲染函数外,避免重复创建;3)最长递增子序列:列表 diff 时使用此算法计算最小移动次数,减少 DOM 操作;4)事件缓存:静态事件处理函数缓存复用。这些优化使 Vue 3 的渲染性能显著提升。
// Vue 3 diff 核心逻辑简化示例
function patchKeyedChildren(c1, c2, container) {
let i = 0, e1 = c1.length - 1, e2 = c2.length - 1;
// 1. 从头对比相同节点
while (i <= e1 && i <= e2) {
if (isSameVNodeType(c1[i], c2[i])) patch(c1[i], c2[i]);
else break;
i++;
}
// 2. 从尾对比相同节点
// 3. 处理新增/删除节点
// 4. 未知序列使用最长递增子序列算法移动节点
}
关联知识点:虚拟 DOM、diff 算法、最长递增子序列、编译优化
Q20. Vue 3 的响应式系统相比 Vue 2 有哪些底层改进?
答案:Vue 2 使用 Object.defineProperty 实现响应式,存在以下限制:无法检测对象属性的添加或删除;无法检测数组索引和长度的变化;需要递归遍历所有属性进行转换。Vue 3 使用 ES6 Proxy 重写响应式系统:1)完整拦截:Proxy 可拦截 13 种操作,包括属性新增、删除、数组方法调用;2)惰性转换:只在访问嵌套对象时才递归代理,提升初始化性能;3)集合类型支持:原生支持 Map、Set、WeakMap、WeakSet;4)更好的 TypeScript 类型推导;5)独立的响应式包:@vue/reactivity 可脱离 Vue 单独使用。
// Vue 3 reactive 简化实现
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key); // 收集依赖
const res = Reflect.get(target, key, receiver);
return isObject(res) ? reactive(res) : res; // 惰性深层代理
},
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;
}
});
}
关联知识点:Proxy、Object.defineProperty、响应式原理、性能优化
Q21. Vue 中如何实现一个自定义的响应式状态管理(不依赖 Vuex/Pinia)?
答案:利用 Vue 3 的 reactive 或 ref 可以构建轻量级的状态管理。核心思路:创建全局共享的响应式对象,通过 composable 函数暴露状态和操作方法。这种方式适合中小型项目,避免引入完整的状态管理库。
// stores/user.js
import { reactive, readonly } from 'vue';
const state = reactive({
user: null,
token: null,
isLoading: false,
});
export function useUserStore() {
const login = async (credentials) => {
state.isLoading = true;
try {
const res = await fetch('/api/login', { method: 'POST', body: JSON.stringify(credentials) });
const data = await res.json();
state.user = data.user;
state.token = data.token;
} finally {
state.isLoading = false;
}
};
const logout = () => {
state.user = null;
state.token = null;
};
return { user: readonly(state), isLoading: readonly(state.isLoading), login, logout };
}
在组件中使用:const { user, login } = useUserStore()。使用 readonly 防止外部直接修改状态,确保状态变更只能通过定义的方法进行,这是单向数据流的实践。
关联知识点:状态管理、组合式 API、readonly、单向数据流
Q22. Vue 3 的编译时优化(静态提升、Patch Flags、Block Tree)是如何工作的?
答案:Vue 3 在模板编译阶段进行静态分析,生成更高效的渲染代码:1)静态提升(Static Hoisting):将静态节点(不含动态绑定的节点)提升到渲染函数外部,只创建一次,避免每次渲染重复创建 VNode;2)Patch Flags:在动态节点上标记更新类型(如 TEXT 表示文本动态、CLASS 表示 class 动态),diff 时只对比标记的属性,跳过静态部分;3)Block Tree:将模板按动态节点边界划分为 Block,每个 Block 收集内部的动态节点为一维数组,diff 时只需遍历动态节点,时间复杂度从 O(模板大小) 降为 O(动态节点数量)。这些优化使 Vue 3 在大型模板中性能显著优于 Vue 2。
关联知识点:编译优化、模板编译、渲染性能、虚拟 DOM
Q23. 设计模式在 Vue 项目中有哪些典型应用?
答案:1)观察者模式:Vue 的响应式系统,通过 track/trigger 实现依赖收集和通知更新;2)发布-订阅模式:组件间的 emit/on 事件机制、Pinia 的 store 订阅;3)组合模式:组件树结构,父子组件嵌套形成树形结构,统一处理渲染逻辑;4)策略模式:动态组件 <component :is="currentComponent"> 根据条件切换渲染策略;5)代理模式:reactive 通过 Proxy 代理对象访问,拦截并增强行为;6)装饰器模式:自定义指令和 Mixins/Composables 对组件功能进行增强;7)单例模式:全局状态管理(Pinia store)、路由实例。理解这些模式有助于写出更优雅、可维护的 Vue 代码。
// 策略模式示例:动态表单渲染
const formStrategies = {
text: () => defineAsyncComponent(() => import('./TextField.vue')),
select: () => defineAsyncComponent(() => import('./SelectField.vue')),
date: () => defineAsyncComponent(() => import('./DateField.vue')),
};
// 根据字段类型动态加载组件
const getComponent = (type) => formStrategies[type]?.();
关联知识点:设计模式、观察者模式、策略模式、代理模式
四、实战场景题 🛠️
Q24. 场景:如何实现一个支持万级数据渲染的虚拟列表?
答案:虚拟列表只渲染可视区域内的元素,通过计算滚动位置确定渲染范围,大幅减少 DOM 节点数量。核心实现:
<template>
<div class="virtual-list" :style="{ height: `${containerHeight}px` }" @scroll="onScroll">
<div class="virtual-list-phantom" :style="{ height: `${totalHeight}px` }"></div>
<div class="virtual-list-content" :style="{ transform: `translateY(${offsetY}px)` }">
<div v-for="item in visibleData" :key="item.id" class="list-item">
{{ item.name }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({ list: Array, itemHeight: Number, containerHeight: Number });
const scrollTop = ref(0);
const visibleCount = computed(() => Math.ceil(props.containerHeight / props.itemHeight) + 2);
const startIndex = computed(() => Math.floor(scrollTop.value / props.itemHeight));
const visibleData = computed(() => props.list.slice(startIndex.value, startIndex.value + visibleCount.value));
const totalHeight = computed(() => props.list.length * props.itemHeight);
const offsetY = computed(() => startIndex.value * props.itemHeight);
const onScroll = (e) => { scrollTop.value = e.target.scrollTop; };
</script>
关键点:使用 phantom 占位元素撑开滚动高度,content 通过 transform 偏移到正确位置,缓冲区多渲染 2 项避免滚动白屏。
关联知识点:虚拟列表、性能优化、大数据渲染、滚动计算
Q25. 场景:如何实现基于角色的动态权限控制(路由级 + 按钮级)?
答案:权限控制分为路由级和按钮级。路由级通过导航守卫和动态路由实现,按钮级通过自定义指令或组件控制。
// router/permission.js - 路由级权限
import router from './index';
import { useUserStore } from '@/stores/user';
const whiteList = ['/login', '/404'];
router.beforeEach(async (to) => {
const userStore = useUserStore();
if (!userStore.token) {
return whiteList.includes(to.path) ? true : `/login?redirect=${to.path}`;
}
// 动态添加路由:根据用户角色加载对应路由
if (!userStore.routesLoaded) {
const accessRoutes = await userStore.generateRoutes();
accessRoutes.forEach(route => router.addRoute(route));
return { ...to, replace: true }; // 重新导航
}
});
// directives/permission.js - 按钮级权限
import { useUserStore } from '@/stores/user';
export const vPermission = {
mounted(el, binding) {
const { value } = binding;
const userStore = useUserStore();
const roles = userStore.roles;
if (value && !roles.some(role => value.includes(role))) {
el.parentNode?.removeChild(el); // 无权限则移除
}
}
};
<!-- 使用 -->
<el-button v-permission="['admin', 'editor']">编辑</el-button>
关联知识点:权限控制、动态路由、自定义指令、导航守卫
Q26. 场景:复杂表单如何实现多步骤验证和状态管理?
答案:复杂表单推荐使用组合式 API 封装表单逻辑,配合验证库(如 VeeValidate + Zod)实现类型安全的验证。
<template>
<form @submit.prevent="handleSubmit">
<!-- Step 1: 基本信息 -->
<div v-show="step === 1">
<input v-model="form.name" placeholder="姓名" />
<span v-if="errors.name">{{ errors.name }}</span>
</div>
<!-- Step 2: 详细信息 -->
<div v-show="step === 2">
<input v-model="form.email" type="email" placeholder="邮箱" />
<span v-if="errors.email">{{ errors.email }}</span>
</div>
<button type="button" @click="prevStep" :disabled="step === 1">上一步</button>
<button type="button" v-if="step < 2" @click="nextStep">下一步</button>
<button type="submit" v-if="step === 2">提交</button>
</form>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { useForm } from 'vee-validate';
import * as z from 'zod';
const step = ref(1);
const { defineField, errors, validate } = useForm({
validationSchema: z.object({
name: z.string().min(2, '姓名至少2个字符'),
email: z.string().email('邮箱格式不正确'),
}),
});
const [name] = defineField('name');
const [email] = defineField('email');
const form = reactive({ name, email });
const nextStep = async () => {
const valid = await validate();
if (valid) step.value++;
};
const handleSubmit = async () => {
const valid = await validate();
if (valid) { /* 提交逻辑 */ }
};
</script>
关键点:每步验证通过后才允许进入下一步,使用 Zod 定义类型安全的验证规则,验证错误自动绑定到字段。 关联知识点:表单验证、VeeValidate、Zod、多步骤表单
Q27. 场景:如何实现一个支持撤销/重做(Undo/Redo)的编辑器状态管理?
答案:使用命令模式(Command Pattern)记录每次操作,维护历史栈实现撤销/重做。
// composables/useUndoRedo.js
import { ref, shallowRef } from 'vue';
export function useUndoRedo(initialState) {
const state = shallowRef(initialState);
const history = ref([initialState]);
const currentIndex = ref(0);
const pushState = (newState) => {
// 撤销中间状态:如果当前不在最后,删除之后的历史记录
history.value = history.value.slice(0, currentIndex.value + 1);
history.value.push(newState);
currentIndex.value++;
state.value = newState;
};
const undo = () => {
if (currentIndex.value > 0) {
currentIndex.value--;
state.value = history.value[currentIndex.value];
}
};
const redo = () => {
if (currentIndex.value < history.value.length - 1) {
currentIndex.value++;
state.value = history.value[currentIndex.value];
}
};
return { state, pushState, undo, redo, canUndo: () => currentIndex.value > 0, canRedo: () => currentIndex.value < history.value.length - 1 };
}
<!-- 使用 -->
<script setup>
import { useUndoRedo } from '@/composables/useUndoRedo';
import { computed } from 'vue';
const { state, pushState, undo, redo, canUndo, canRedo } = useUndoRedo({ text: '' });
const updateText = (newText) => pushState({ text: newText });
</script>
使用 shallowRef 避免深层响应式转换,提升大对象性能。可添加最大历史记录数限制防止内存溢出。
关联知识点:命令模式、状态管理、shallowRef、撤销重做
Q28. 场景:如何排查和解决 Vue 应用的性能问题?
答案:Vue 应用性能优化需要从多个维度排查和优化:
排查工具:
- Vue DevTools:查看组件渲染时间、props 变化、组件树结构
- Chrome Performance 面板:分析长任务、强制重排、垃圾回收
@vue/devtools性能追踪:app.config.performance = true开启组件渲染性能标记
常见优化策略:
// 1. 大列表使用虚拟滚动(见 Q24)
// 2. 避免不必要的响应式转换
import { shallowRef, shallowReactive } from 'vue';
const largeData = shallowRef([]); // 不深层代理
// 3. 使用 v-once 渲染静态内容
<div v-once>{{ staticContent }}</div>
// 4. 拆分大组件,使用异步组件
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'));
// 5. 合理使用 computed 缓存派生数据
const filteredList = computed(() => list.value.filter(item => item.active));
// 6. 避免在模板中使用函数调用(每次渲染都执行)
// 错误:{{ formatPrice(price) }}
// 正确:使用 computed
关键指标:首次内容渲染(FCP)< 1.8s,交互就绪(TTI)< 3.8s,组件渲染时间 < 16ms(60fps)。 关联知识点:性能优化、Vue DevTools、shallowRef、异步组件、计算属性