Vue 前端开发 - 面试题库


一、基础题 ⭐

Q1. Vue 3 的响应式系统是如何工作的?

答案:Vue 3 使用 Proxy 对象实现响应式。reactive() 将对象包装为 Proxy,拦截 getset 操作。在 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() 本身在 beforeCreatecreated 之间执行,替代了这两个钩子。 关联知识点:生命周期、组合式 API、DOM 操作

Q3. v-ifv-show 的区别是什么?如何选择?

答案v-if 是真正的条件渲染,条件为假时组件不会渲染到 DOM 中,切换时会销毁和重建组件,触发完整的生命周期。v-show 始终渲染组件,仅通过 CSS display: none 控制显隐,切换开销小。选择原则:频繁切换的场景用 v-show(如 Tab 切换、弹窗),运行时条件很少改变或初始为假的场景用 v-if(如权限控制、懒加载组件)。v-if 支持 <template> 分组,v-show 不支持。 关联知识点:条件渲染、性能优化、DOM 操作

Q4. Vue 中 refreactive 的区别是什么?

答案ref 用于创建基本类型或对象的响应式引用,通过 .value 访问值,在模板中自动解包无需 .valuereactive 仅适用于对象类型(对象、数组、Map、Set),返回 Proxy 代理对象,无需 .valueref 内部对于对象类型也会调用 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,执行异步操作或开销大的操作用 watchcomputed 默认只读,可定义 getset 实现可写计算属性。 关联知识点:计算属性、侦听器、缓存机制

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-bindv-onv-model)。自定义指令通过 app.directive('focus', { mounted(el) { el.focus() } }) 注册,生命周期钩子包括 created(绑定元素属性或事件监听器设置前)、mounted(元素插入父节点时)、beforeUpdate(更新前)、updatedbeforeUnmountunmounted。适用场景:自动聚焦、图片懒加载、权限按钮控制、拖拽等需要直接操作 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(离开前)。完整导航流程:beforeRouteLeavebeforeEachbeforeRouteUpdate(复用场景)→ beforeEnterbeforeRouteEnterbeforeResolveafterEach。守卫中调用 next(false) 取消导航,next('/path') 重定向,next(error) 中断并传递错误。 关联知识点:导航守卫、路由、权限控制

Q13. Vue 中如何实现路由懒加载?原理是什么?

答案:路由懒加载通过动态 import() 语法实现:const Home = () => import('./views/Home.vue')。Webpack 或 Vite 会将该组件打包为独立的 chunk,在路由首次访问时才加载该 chunk,减少初始包体积。配合 Vue Router 使用时,可结合 loadingComponentdelay 配置加载状态。对于大型应用,可按路由模块拆分 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 策略淘汰)。配合路由使用时,被缓存的组件触发 activateddeactivated 钩子,而非 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-modelv-model:title 对应 title + update:title。修饰符如 .lazy.number.trim 在自定义组件中需在组件内部处理。 关联知识点:双向绑定、语法糖、自定义组件事件

Q18. Vue 中的 Teleport(传送门)是什么?使用场景有哪些?

答案<Teleport> 是 Vue 3 内置组件,将组件的 DOM 渲染到指定的 DOM 节点下,而非组件树中的位置。语法:<Teleport to="body">to="#modal-root"。解决 CSS 层级问题:模态框、通知、下拉菜单等组件如果嵌套在 overflow: hiddenz-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 的 reactiveref 可以构建轻量级的状态管理。核心思路:创建全局共享的响应式对象,通过 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 应用性能优化需要从多个维度排查和优化:

排查工具

  1. Vue DevTools:查看组件渲染时间、props 变化、组件树结构
  2. Chrome Performance 面板:分析长任务、强制重排、垃圾回收
  3. @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、异步组件、计算属性