目录
- 一、基础必答(所有厂都考,正确率必须100%)
-
- [1. Vue 核心基础](#1. Vue 核心基础)
-
- [1. Vue2 和 Vue3 响应式原理的区别?各自的优缺点?](#1. Vue2 和 Vue3 响应式原理的区别?各自的优缺点?)
- [2. v-if 和 v-show 的区别及使用场景?](#2. v-if 和 v-show 的区别及使用场景?)
- [3. computed 和 watch 的区别?watchEffect 对比 watch 有什么不同?](#3. computed 和 watch 的区别?watchEffect 对比 watch 有什么不同?)
- [4. 组件间通信有哪些方式?(至少说出6种,重点说跨层级方案)](#4. 组件间通信有哪些方式?(至少说出6种,重点说跨层级方案))
- [5. v-for 为什么要加 key?为什么不能用 index 作为 key?](#5. v-for 为什么要加 key?为什么不能用 index 作为 key?)
- [6. Vue 的生命周期钩子(Vue2/Vue3)?setup 执行时机?](#6. Vue 的生命周期钩子(Vue2/Vue3)?setup 执行时机?)
- [7. 双向绑定 v-model 的原理?Vue3 中 v-model 与 Vue2 的差异?](#7. 双向绑定 v-model 的原理?Vue3 中 v-model 与 Vue2 的差异?)
- [8. 自定义指令的使用场景和实现方式?](#8. 自定义指令的使用场景和实现方式?)
- [2. Vue 生态基础](#2. Vue 生态基础)
-
- [1. VueRouter 路由守卫有哪些?分别用在什么场景?](#1. VueRouter 路由守卫有哪些?分别用在什么场景?)
- [2. 路由懒加载的实现方式?为什么要做懒加载?](#2. 路由懒加载的实现方式?为什么要做懒加载?)
- [3. Vuex/Pinia 的核心概念?Pinia 对比 Vuex 有什么优势?](#3. Vuex/Pinia 的核心概念?Pinia 对比 Vuex 有什么优势?)
- [4. VueRouter 的 hash 模式和 history 模式区别?history 模式部署需要后端做什么配置?](#4. VueRouter 的 hash 模式和 history 模式区别?history 模式部署需要后端做什么配置?)
- 二、进阶加分(大厂/中厂重点,外包偶尔问)
-
- [1. Vue 原理进阶](#1. Vue 原理进阶)
-
- [1. Vue3 中 Proxy 为什么能解决 Vue2 响应式的缺陷?](#1. Vue3 中 Proxy 为什么能解决 Vue2 响应式的缺陷?)
- [2. Vue 的虚拟 DOM 和 Diff 算法原理?Vue3 的 Diff 做了哪些优化?](#2. Vue 的虚拟 DOM 和 Diff 算法原理?Vue3 的 Diff 做了哪些优化?)
- [3. 组件的异步组件怎么实现?Suspense 组件的使用场景?](#3. 组件的异步组件怎么实现?Suspense 组件的使用场景?)
- [4. Vue3 的 Composition API 对比 Options API 的优势?解决了什么问题?](#4. Vue3 的 Composition API 对比 Options API 的优势?解决了什么问题?)
- [5. Vue 的 mixin 有什么优缺点?为什么推荐用组合式 API 替代 mixin?](#5. Vue 的 mixin 有什么优缺点?为什么推荐用组合式 API 替代 mixin?)
- [6. 谈谈 Vue 的依赖收集和触发更新过程?](#6. 谈谈 Vue 的依赖收集和触发更新过程?)
- [7. nextTick 的原理和使用场景?](#7. nextTick 的原理和使用场景?)
- [2. 工程化 & 性能优化](#2. 工程化 & 性能优化)
-
- [1. Vue 项目中如何做性能优化?(至少5点,结合渲染、打包、运行时)](#1. Vue 项目中如何做性能优化?(至少5点,结合渲染、打包、运行时))
- [2. Webpack 打包 Vue 项目时,如何做体积优化?](#2. Webpack 打包 Vue 项目时,如何做体积优化?)
- [3. Vite 为什么比 Webpack 快?Vite 构建 Vue 项目的流程?](#3. Vite 为什么比 Webpack 快?Vite 构建 Vue 项目的流程?)
- [4. 大型 Vue 项目中,如何做状态管理的拆分?](#4. 大型 Vue 项目中,如何做状态管理的拆分?)
- [5. Vue 项目中如何实现权限控制?(路由权限、按钮权限)](#5. Vue 项目中如何实现权限控制?(路由权限、按钮权限))
- [6. 虚拟滚动在 Vue 中的实现方式?](#6. 虚拟滚动在 Vue 中的实现方式?)
- 三、架构拔高(大厂必考,中厂选考,外包基本不考)
-
-
- [1. 如何设计一个可复用的 Vue 组件库?](#1. 如何设计一个可复用的 Vue 组件库?)
- [2. Vue 项目中如何实现微前端?(qiankun 接入流程、样式隔离、通信)](#2. Vue 项目中如何实现微前端?(qiankun 接入流程、样式隔离、通信))
- [3. Vue 服务端渲染(SSR)的原理?Nuxt.js 的核心流程?SSR 对比 CSR 的优劣势?](#3. Vue 服务端渲染(SSR)的原理?Nuxt.js 的核心流程?SSR 对比 CSR 的优劣势?)
- [4. 大型 Vue 项目的目录结构如何设计?](#4. 大型 Vue 项目的目录结构如何设计?)
- [5. Vue3 + TypeScript 开发时,如何做类型约束?](#5. Vue3 + TypeScript 开发时,如何做类型约束?)
- [6. **如何做 Vue 项目的错误监控和性能监控?**](#6. 如何做 Vue 项目的错误监控和性能监控?)
- [7. Vue 项目跨端方案(uni-app/Taro)的选型和踩坑点?](#7. Vue 项目跨端方案(uni-app/Taro)的选型和踩坑点?)
-
- 四、手写代码(所有厂都可能现场考,优先掌握)
-
-
- [1. 手写 Vue3 自定义 hook(如:useDebounce、useLocalStorage)](#1. 手写 Vue3 自定义 hook(如:useDebounce、useLocalStorage))
- [2. 手写简易版 Vue 响应式(Vue3 Proxy 版本)](#2. 手写简易版 Vue 响应式(Vue3 Proxy 版本))
- [3. 手写一个通用的 Vue 分页组件(支持自定义配置、事件回调)](#3. 手写一个通用的 Vue 分页组件(支持自定义配置、事件回调))
- [4. 实现 Vue 中的防抖/节流指令(v-debounce/v-throttle)](#4. 实现 Vue 中的防抖/节流指令(v-debounce/v-throttle))
- [5. 手写 Pinia 持久化插件(基于 localStorage)](#5. 手写 Pinia 持久化插件(基于 localStorage))
-
- 五、场景题(大厂/中厂重点,考察落地能力)
-
-
- [1. 场景1:Vue 项目首屏加载慢,你如何定位问题并优化?](#1. 场景1:Vue 项目首屏加载慢,你如何定位问题并优化?)
- [2. 场景2:多人协作开发 Vue 项目,如何保证代码规范和质量?](#2. 场景2:多人协作开发 Vue 项目,如何保证代码规范和质量?)
- [3. 场景3:Vue 项目中遇到内存泄漏,你如何排查和解决?](#3. 场景3:Vue 项目中遇到内存泄漏,你如何排查和解决?)
- [4. 场景4:移动端 Vue 项目适配方案?](#4. 场景4:移动端 Vue 项目适配方案?)
-
- 总结
一、基础必答(所有厂都考,正确率必须100%)
1. Vue 核心基础
1. Vue2 和 Vue3 响应式原理的区别?各自的优缺点?
- Vue2 :基于
Object.defineProperty劫持对象属性的get/set,遍历对象的每个属性实现响应式。- 优点:兼容性好(支持 IE9+),逻辑简单易懂;
- 缺点:
① 无法监听数组下标修改、数组长度变化;
② 无法监听对象新增/删除属性;
③ 需递归遍历对象,性能随对象层级加深下降。
- Vue3 :基于
Proxy代理整个对象,而非单个属性。- 优点:
① 天然支持数组/对象的所有操作(新增/删除/下标修改);
② 非侵入式(无需修改原对象);
③ 懒代理(访问属性时才递归子对象,性能更优); - 缺点:兼容性差(不支持 IE),需通过编译降级兼容。
- 优点:
2. v-if 和 v-show 的区别及使用场景?
- 核心区别 :
- v-if:「条件渲染」,不满足条件时组件不会渲染(DOM 不存在),切换时会触发组件生命周期(创建/销毁);
- v-show:「样式隐藏」,组件始终渲染(DOM 存在),仅通过
display: none控制显示/隐藏,切换仅修改样式。
- 使用场景 :
- v-if:条件少变(如权限控制、首次加载根据接口数据渲染),适合节省初始渲染开销;
- v-show:条件频繁切换(如 tab 切换、弹窗显隐),适合减少频繁的 DOM 操作开销。
3. computed 和 watch 的区别?watchEffect 对比 watch 有什么不同?
-
computed vs watch :
维度 computed watch 本质 计算属性,依赖缓存 监听属性,无缓存 触发时机 依赖变化时自动计算 监听值变化时执行回调 返回值 必须返回一个值 无返回值(仅执行逻辑) 使用场景 衍生值计算(如拼接字符串、过滤列表) 异步操作、复杂逻辑(如监听输入框变化请求接口) -
watchEffect vs watch :
- watch:需显式指定监听的数据源(如
watch(() => state.count, () => {})),可获取新旧值; - watchEffect:隐式监听回调内用到的所有响应式数据,无需指定数据源,无法获取新旧值,初始化时会立即执行一次。
- watch:需显式指定监听的数据源(如
4. 组件间通信有哪些方式?(至少说出6种,重点说跨层级方案)
- ① Props / Emits:父子通信(父传子用 props,子传父用 emits),最基础;
- ② v-model:父子双向绑定(语法糖,本质是 props + emits);
- ③ ref / parent / children:父通过 ref 获取子组件实例,子通过 $parent 获取父实例(不推荐,耦合度高);
- ④ provide / inject:跨层级通信(祖组件 provide 提供数据,后代组件 inject 注入),适合深层嵌套组件;
- ⑤ Pinia/Vuex:全局状态管理,适合任意组件间通信(尤其是跨页面、跨层级);
- ⑥ 事件总线(mitt):通过发布/订阅模式通信,适合中小型项目的非核心数据通信;
- ⑦ attrs / listeners:透传属性/事件(父传孙,无需中间组件转发);
- ⑧ VueRouter 路由参数:跨页面通信(query/params)。
5. v-for 为什么要加 key?为什么不能用 index 作为 key?
- key 的作用:作为虚拟 DOM 的唯一标识,Vue Diff 算法通过 key 识别节点的「复用性」,避免不必要的 DOM 重建,提升渲染性能。
- 不能用 index 作为 key 的原因 :
当列表数据重新排序/增删时,index 会随位置变化(如删除第1项,原第2项的 index 变为0),导致 Vue 误判「节点已变化」,触发不必要的 DOM 卸载/重建,甚至引发数据与 DOM 不匹配的 bug(如输入框值错乱)。 - 推荐:用数据的唯一标识(如 id、uuid)作为 key。
6. Vue 的生命周期钩子(Vue2/Vue3)?setup 执行时机?
- Vue2 生命周期 :
创建阶段:beforeCreate → created(可访问数据,DOM 未生成);
挂载阶段:beforeMount → mounted(DOM 渲染完成,可操作 DOM);
更新阶段:beforeUpdate → updated(数据更新,DOM 重新渲染);
销毁阶段:beforeDestroy → destroyed(组件销毁,清除定时器/事件)。 - Vue3 生命周期(组合式 API) :
替换:beforeCreate/created → setup;
其余:onMounted、onUpdated、onUnmounted(对应 mounted/updated/destroyed);
新增:onRenderTracked(追踪渲染依赖)、onRenderTriggered(触发渲染时)。 - setup 执行时机:在 beforeCreate 之前执行(此时 this 为 undefined),仅执行一次,用于初始化组合式 API 的数据和方法。
7. 双向绑定 v-model 的原理?Vue3 中 v-model 与 Vue2 的差异?
- 原理 :语法糖,Vue2 中
v-model="value"等价于:value="value" @input="value = $event.target.value"; - Vue2 vs Vue3 :
- Vue2:一个组件只能有一个 v-model,默认绑定
value属性 +input事件; - Vue3:支持多个 v-model,可自定义绑定的属性和事件(如
v-model:name绑定name属性 +update:name事件),取消.sync修饰符(统一用 v-model 替代)。
- Vue2:一个组件只能有一个 v-model,默认绑定
8. 自定义指令的使用场景和实现方式?
-
使用场景:操作 DOM 相关的逻辑(如防抖/节流、输入框自动聚焦、图片懒加载、权限按钮隐藏);
-
实现方式(Vue3):
html<template> <input v-focus /> </template> <script setup> // 全局指令 import { app } from './main' app.directive('focus', { mounted(el) { el.focus() } // 指令钩子:mounted 时执行 }) // 局部指令 const vFocus = { mounted(el) { el.focus() } } </script> -
核心钩子:created(元素创建)、mounted(元素挂载)、updated(元素更新)、unmounted(元素卸载)。
2. Vue 生态基础
1. VueRouter 路由守卫有哪些?分别用在什么场景?
- 按范围分三类:
① 全局守卫 :router.beforeEach:路由跳转前(如登录验证、权限控制);router.afterEach:路由跳转后(如页面埋点、修改标题);
② 路由独享守卫 :beforeEnter(定义在路由配置中,仅当前路由生效,如详情页权限);
③ 组件内守卫:beforeRouteEnter:进入组件前(无法访问 this,需通过回调);beforeRouteUpdate:路由参数变化(如 /user/1 → /user/2,组件复用);beforeRouteLeave:离开组件前(如提示未保存表单)。
2. 路由懒加载的实现方式?为什么要做懒加载?
-
目的:拆分代码包,减少首屏加载的 JS 体积,提升首屏速度;
-
实现方式(Vue3) :
js// 路由配置中使用 import 动态导入 const routes = [ { path: '/home', component: () => import('@/views/Home.vue') // 懒加载 } ] // 按需分组(打包到同一个 chunk) const routes = [ { path: '/user', component: () => import(/* webpackChunkName: "user" */ '@/views/User.vue') } ]
3. Vuex/Pinia 的核心概念?Pinia 对比 Vuex 有什么优势?
- Vuex 核心:State(状态)、Getter(计算状态)、Mutation(同步修改)、Action(异步修改)、Module(模块化);
- Pinia 核心:State(状态)、Getter(计算状态)、Action(同步/异步修改)(无 Mutation、Module);
- Pinia 优势 :
① 简化 API(无需 Mutation,Action 支持同步/异步);
② 天然支持 TypeScript,类型提示更友好;
③ 无需嵌套模块化,通过多个 store 实现模块化;
④ 体积更小,性能更优;
⑤ 兼容 Vue2/Vue3。
4. VueRouter 的 hash 模式和 history 模式区别?history 模式部署需要后端做什么配置?
-
核心区别 :
维度 hash 模式 history 模式 URL 表现 带 #(如 /#/home) 无 #(如 /home) 底层原理 监听 hashchange 事件 基于 H5 History API 刷新页面 不会发送到服务器 会发送到服务器 兼容性 支持 IE8+ 支持 IE10+ -
history 模式部署要求 :
后端需配置「兜底路由」(如 Nginx),将所有非静态资源的请求转发到 index.html,避免刷新 404:nginxlocation / { try_files $uri $uri/ /index.html; }
二、进阶加分(大厂/中厂重点,外包偶尔问)
1. Vue 原理进阶
1. Vue3 中 Proxy 为什么能解决 Vue2 响应式的缺陷?
- Vue2 缺陷:
① 数组:Object.defineProperty无法监听arr[0] = 1、arr.length = 0等操作,需重写数组方法(push/pop 等);
② 对象:无法监听新增/删除属性,需手动调用Vue.set/Vue.delete; - Proxy 优势:
① 代理整个对象,而非单个属性,天然支持数组下标/长度修改;
② 能捕获deleteProperty(删除属性)、set(新增/修改属性)等操作,无需手动干预;
③ 支持更多操作(如has监听in操作、ownKeys监听Object.keys)。
2. Vue 的虚拟 DOM 和 Diff 算法原理?Vue3 的 Diff 做了哪些优化?
- 虚拟 DOM :用 JS 对象描述 DOM 结构(如
{ tag: 'div', props: { class: 'box' }, children: [] }),避免直接操作真实 DOM,通过对比新旧虚拟 DOM 差异,只更新需要变化的 DOM。 - Diff 算法核心 :
① 同级比较(不跨层级);
② 先判断 key 是否相同,相同则复用节点,仅更新 props;
③ 不同则销毁旧节点,创建新节点; - Vue3 Diff 优化 :
① 静态提升 :静态节点(如<div>文本</div>)只创建一次,复用;
② PatchFlags :标记动态节点(如仅 props 变化、仅文本变化),Diff 时只遍历标记的节点;
③ 最长递增子序列:优化列表 Diff,减少 DOM 移动次数(如列表排序时,仅移动必要节点)。
3. 组件的异步组件怎么实现?Suspense 组件的使用场景?
-
异步组件实现(Vue3) :
js<script setup> // 基础用法 const AsyncComponent = defineAsyncComponent(() => import('@/components/Async.vue')) // 高级用法(加载中/错误处理) const AsyncComponent = defineAsyncComponent({ loader: () => import('@/components/Async.vue'), loadingComponent: () => import('@/components/Loading.vue'), // 加载中组件 errorComponent: () => import('@/components/Error.vue'), // 加载失败组件 delay: 200, // 延迟显示加载组件(避免闪屏) timeout: 3000 // 超时时间 }) </script> -
Suspense 场景 :包裹异步组件,统一处理「加载中」和「加载完成」状态,无需每个异步组件单独写 loading 逻辑:
html<template> <Suspense> <template #default> <AsyncComponent /> </template> <template #fallback> <div>加载中...</div> </template> </Suspense> </template>
4. Vue3 的 Composition API 对比 Options API 的优势?解决了什么问题?
- Options API 问题 :
① 逻辑分散(数据在 data、方法在 methods、监听在 watch),复杂组件难以维护;
② 逻辑复用困难(mixin 命名冲突、来源不清晰); - Composition API 优势 :
① 逻辑聚合(相关逻辑写在 setup 中,如「表单验证」的 data + methods + watch 放在一起);
② 逻辑复用灵活(自定义 hook,如 useForm、useList,无命名冲突);
③ 更好的 TypeScript 支持;
④ 按需引入,体积更小。
5. Vue 的 mixin 有什么优缺点?为什么推荐用组合式 API 替代 mixin?
- mixin 优点:实现逻辑复用(如多个组件的表单验证逻辑);
- mixin 缺点 :
① 命名冲突(mixin 和组件的 data/methods 重名时,组件覆盖 mixin);
② 来源不清晰(组件中无法直观看到 mixin 的逻辑);
③ 逻辑耦合(mixin 之间可能相互依赖); - 组合式 API 替代原因:自定义 hook 可明确传入/传出参数,逻辑边界清晰,无命名冲突,可读性更高。
6. 谈谈 Vue 的依赖收集和触发更新过程?
- 依赖收集 :
① 组件渲染时,执行 render 函数,访问响应式数据,触发get拦截;
②get拦截器中将当前组件的「副作用函数(更新函数)」收集到「依赖集合」中; - 触发更新 :
① 响应式数据修改,触发set拦截;
②set拦截器中遍历「依赖集合」,执行所有副作用函数,更新组件。 - 核心:每个响应式数据对应一个依赖集合,收集使用该数据的组件,数据变化时通知组件更新。
7. nextTick 的原理和使用场景?
-
原理 :Vue 异步更新 DOM(数据变化后,不会立即更新 DOM,而是将更新任务放入队列),
nextTick用于在 DOM 更新完成后执行回调; -
实现:优先使用微任务(Promise.then),降级使用宏任务(setTimeout);
-
使用场景 :
① 数据修改后,立即操作更新后的 DOM(如获取输入框焦点、计算 DOM 尺寸);
② 批量修改数据,避免多次 DOM 更新(如循环修改数据,最后调用 nextTick 统一处理)。js<script setup> const count = ref(0) const handleClick = () => { count.value = 1 console.log(document.querySelector('.count').innerText) // 旧值 0 nextTick(() => { console.log(document.querySelector('.count').innerText) // 新值 1 }) } </script>
2. 工程化 & 性能优化
1. Vue 项目中如何做性能优化?(至少5点,结合渲染、打包、运行时)
- 渲染优化 :
① 减少响应式数据(非响应式数据用markRaw标记,避免 Proxy 代理);
② 避免不必要的渲染(用computed缓存、v-once标记静态节点);
③ 列表优化(v-for 加 key、虚拟滚动处理长列表); - 打包优化 :
① 路由懒加载、组件懒加载;
② 按需引入第三方库(如 Element Plus 按需导入);
③ 压缩代码(Terser 压缩 JS、css-minimizer 压缩 CSS);
④ 分包(splitChunks 拆分公共代码、CDN 引入大库如 Vue/Element Plus); - 运行时优化 :
① 防抖节流(如搜索框输入、按钮点击);
② 图片优化(懒加载、webp 格式、雪碧图);
③ 减少 DOM 操作(用虚拟 DOM、批量修改数据);
④ 缓存接口数据(localStorage/Pinia,避免重复请求)。
2. Webpack 打包 Vue 项目时,如何做体积优化?
-
① Tree Shaking :开启
mode: production(默认开启),删除未使用的代码; -
② 按需引入:第三方库(如 Element Plus、VueUse)按需导入,而非全量引入;
-
③ 代码分割 :
splitChunks拆分公共代码、第三方库:js// webpack.config.js module.exports = { optimization: { splitChunks: { chunks: 'all', // 拆分所有 chunk cacheGroups: { vendor: { // 拆分第三方库 test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all' } } } } } -
④ CDN 引入:将 Vue、VueRouter、Pinia 等大库通过 CDN 引入,排除在打包之外;
-
⑤ 压缩资源 :使用
terser-webpack-plugin压缩 JS,css-minimizer-webpack-plugin压缩 CSS; -
⑥ 分析体积 :用
webpack-bundle-analyzer分析打包体积,定位大文件。
3. Vite 为什么比 Webpack 快?Vite 构建 Vue 项目的流程?
- Vite 快的原因 :
① 开发阶段 :无需打包,基于 ES Module 直接按需编译(浏览器请求时才编译文件);
② 预构建 :用 esbuild(Go 编写)预构建第三方库(将 CommonJS 转为 ESM),比 Webpack 的 babel 快 10-100 倍;
③ HMR 优化 :仅更新修改的模块,无需重新打包;
④ 生产构建:用 Rollup 打包(比 Webpack 更适合库打包,体积更小); - Vite 构建流程 :
① 开发阶段:启动开发服务器 → 预构建第三方库 → 浏览器请求文件 → esbuild 编译文件 → 返回给浏览器;
② 生产阶段:执行vite build→ 预构建 → Rollup 打包 → 生成静态资源。
4. 大型 Vue 项目中,如何做状态管理的拆分?
- ① 按业务模块拆分 store:如用户模块(userStore)、商品模块(goodsStore)、订单模块(orderStore);
- ② 按功能拆分 :
- 全局状态(如用户信息、权限):放在根 store;
- 页面级状态:放在页面组件的 setup 中(无需放入全局 store);
- 跨页面状态:放在对应业务 store;
- ③ 命名空间/前缀:Pinia 无需嵌套,直接通过 store 名称区分(如 useUserStore、useGoodsStore);
- ④ 持久化:核心状态(如用户 token)通过 Pinia 插件(pinia-plugin-persistedstate)持久化到 localStorage;
- ⑤ 避免过度封装:简单状态(如单个页面的表单)用组件内的 ref/reactive 即可,无需放入全局 store。
5. Vue 项目中如何实现权限控制?(路由权限、按钮权限)
-
路由权限 :
① 路由配置中添加meta: { roles: ['admin', 'editor'] };
② 全局路由守卫beforeEach中判断用户角色,无权限则跳转到 403/登录页:jsrouter.beforeEach((to, from, next) => { const userRoles = localStorage.getItem('roles')?.split(',') || [] if (to.meta.roles && !to.meta.roles.some(role => userRoles.includes(role))) { next('/403') } else { next() } }) -
按钮权限 :
① 自定义指令v-permission:jsapp.directive('permission', { mounted(el, binding) { const userRoles = localStorage.getItem('roles')?.split(',') || [] if (!userRoles.includes(binding.value)) { el.style.display = 'none' } } })② 使用:
<button v-permission="'admin'">删除</button>; -
接口权限:请求拦截器中携带 token,后端验证权限,前端处理 403 响应。
6. 虚拟滚动在 Vue 中的实现方式?
-
场景:长列表(如 10000 条数据),仅渲染可视区域的 DOM,提升性能;
-
实现方式 :
① 第三方库:vue-virtual-scroller(Vue2)、@vueuse/core的useVirtualList(Vue3);
② 手写核心逻辑(Vue3):html<template> <div class="list-container" ref="container" @scroll="handleScroll"> <div class="list-content" :style="{ transform: `translateY(${top}px)` }"> <div v-for="item in visibleList" :key="item.id" class="list-item"> {{ item.name }} </div> </div> </div> </template> <script setup> import { ref, computed, onMounted } from 'vue' const list = ref([]) // 所有数据 const container = ref(null) const top = ref(0) const itemHeight = 50 // 每条数据高度 const visibleCount = ref(20) // 可视区域显示条数 // 可视区域数据 const visibleList = computed(() => { const start = Math.floor(container.value.scrollTop / itemHeight) const end = start + visibleCount.value return list.value.slice(start, end) }) // 滚动事件 const handleScroll = () => { const start = Math.floor(container.value.scrollTop / itemHeight) top.value = start * itemHeight } // 模拟数据 onMounted(() => { list.value = Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` })) }) </script> <style> .list-container { height: 500px; overflow: auto; } .list-content { height: 10000 * 50px; position: relative; } .list-item { height: 50px; line-height: 50px; } </style>
三、架构拔高(大厂必考,中厂选考,外包基本不考)
1. 如何设计一个可复用的 Vue 组件库?
-
① 封装原则 :
- 单一职责(一个组件只做一件事);
- 可配置化(通过 props 暴露配置项,如按钮的 size/type/disabled);
- 低耦合(组件内部逻辑独立,不依赖外部状态);
-
② 目录结构 :
packages/ ├── button/ // 按钮组件 │ ├── src/ │ │ ├── button.vue │ │ └── index.ts │ └── package.json ├── input/ // 输入框组件 ├── utils/ // 工具函数 └── index.ts // 入口文件(导出所有组件) -
③ 按需引入 :用
unplugin-vue-components实现自动按需导入; -
④ 类型提示:用 TypeScript 编写,定义 props 类型,提供 d.ts 文件;
-
⑤ 样式处理:支持主题定制(如 CSS 变量、scss 变量),样式隔离(CSS Modules);
-
⑥ 文档&测试:用 VitePress 写文档,Vitest 写单元测试;
-
⑦ 发布:打包为 ES Module/CJS/UMD 格式,发布到 npm。
2. Vue 项目中如何实现微前端?(qiankun 接入流程、样式隔离、通信)
-
核心库:qiankun(基于 single-spa);
-
接入流程 :
① 主应用配置:js// 主应用 main.js import { registerMicroApps, start } from 'qiankun' // 注册子应用 registerMicroApps([ { name: 'vue-app', // 子应用名称 entry: '//localhost:8081', // 子应用地址 container: '#micro-container', // 挂载容器 activeRule: '/vue-app', // 激活路由 } ]) // 启动 qiankun start()② 子应用配置:
js// 子应用 main.js let instance = null export async function bootstrap() {} export async function mount(props) { instance = createApp(App) instance.use(router).mount(props.container.querySelector('#app')) } export async function unmount() { instance.unmount() } -
样式隔离 :
① qiankun 自带样式隔离(沙箱);
② 子应用样式加前缀(如vue-app-),避免冲突; -
通信 :
① 主应用通过 props 传递数据给子应用;
② 全局 EventBus(mitt);
③ 共享 store(如 Pinia)。
3. Vue 服务端渲染(SSR)的原理?Nuxt.js 的核心流程?SSR 对比 CSR 的优劣势?
-
SSR 原理 :
① 服务端:接收请求 → 创建 Vue 实例 → 渲染虚拟 DOM 为 HTML → 返回 HTML 给客户端;
② 客户端:接收 HTML(首屏内容)→ 激活(hydrate)Vue 实例 → 变为可交互的单页应用; -
Nuxt.js 核心流程 :
① 开发阶段:基于 Vue 构建,自动配置 SSR;
② 构建阶段:生成服务端 bundle 和客户端 bundle;
③ 运行阶段:请求 → 中间件 → 页面组件 → 渲染 HTML → 返回客户端; -
SSR vs CSR :
维度 SSR CSR 首屏速度 快(服务端返回完整 HTML) 慢(需下载 JS 后渲染) SEO 友好(搜索引擎可抓取内容) 差(仅抓取空 HTML) 服务器压力 大(需渲染 HTML) 小(仅返回静态资源) 开发复杂度 高(需处理服务端/客户端差异) 低
4. 大型 Vue 项目的目录结构如何设计?
-
核心原则:分层、模块化、业务与基础分离;
src/
├── api/ // 接口请求(按模块拆分)
│ ├── user/ // 用户模块接口
│ ├── goods/ // 商品模块接口
│ └── index.ts // 接口入口
├── assets/ // 静态资源(图片、样式)
├── components/ // 组件
│ ├── base/ // 基础组件(按钮、输入框)
│ ├── business/ // 业务组件(订单列表、商品卡片)
│ └── layout/ // 布局组件(头部、侧边栏)
├── composables/ // 自定义 hook(useForm、useList)
├── directives/ // 自定义指令
├── router/ // 路由(按模块拆分)
├── store/ // 状态管理(按模块拆分)
├── styles/ // 全局样式(变量、重置样式)
├── utils/ // 工具函数(请求、格式化、常量)
├── views/ // 页面组件(按业务模块拆分)
│ ├── user/ // 用户页面
│ ├── goods/ // 商品页面
│ └── order/ // 订单页面
├── App.vue
└── main.ts
5. Vue3 + TypeScript 开发时,如何做类型约束?
-
① Props 类型约束 :
js<script setup lang="ts"> interface Props { name: string age?: number list: Array<{ id: number; text: string }> } const props = defineProps<Props>() // 带默认值 const props = withDefaults(defineProps<Props>(), { age: 18, list: () => [] }) </script> -
② 响应式数据类型约束 :
tsimport { ref, reactive } from 'vue' interface User { name: string age: number } const user = ref<User>({ name: '张三', age: 18 }) const list = reactive<Array<User>>([]) -
③ 函数返回值约束 :
tsconst getUser = (): Promise<User> => { return axios.get('/api/user') } -
④ 全局类型 :在
src/types/index.ts中定义全局类型,在tsconfig.json中配置typeRoots。
6. 如何做 Vue 项目的错误监控和性能监控?
-
错误监控 :
① 全局错误捕获:js// main.js app.config.errorHandler = (err, instance, info) => { // 上报错误(如接口、错误信息、组件信息) axios.post('/api/error', { message: err.message, stack: err.stack, info, // 错误位置(如 render、watch) url: window.location.href }) }② 接口错误捕获:请求拦截器中处理 4xx/5xx 响应;
③ 白屏监控:定时检查 #app 内是否有内容,无则上报; -
性能监控 :
① 使用Performance API监控首屏时间、LCP(最大内容绘制)、FCP(首次内容绘制):jsconst observer = new PerformanceObserver((list) => { const lcp = list.getEntries()[0] // 上报 LCP 时间 axios.post('/api/performance', { lcp: lcp.startTime }) }) observer.observe({ type: 'largest-contentful-paint', buffered: true })② 监控组件渲染时间:在 setup 中记录开始/结束时间;
③ 第三方工具:接入 Sentry、Fundebug 等成熟监控平台。
7. Vue 项目跨端方案(uni-app/Taro)的选型和踩坑点?
-
选型 :
方案 优势 劣势 uni-app 生态完善,支持多端(微信/支付宝小程序、H5、App) 部分 API 与原生小程序有差异 Taro 更好的 TypeScript 支持,React/Vue 双框架 生态不如 uni-app 完善 -
踩坑点 :
① 样式兼容:小程序不支持:hover、position: fixed有坑;
② API 兼容:不同端的 API 差异(如微信小程序的wx.requestvs H5 的fetch);
③ 路由差异:小程序路由与 H5 路由不同,需适配;
④ 性能问题:小程序包体积限制(2M),需分包;
⑤ 生命周期:跨端生命周期有差异(如小程序的onShow/onHide)。
四、手写代码(所有厂都可能现场考,优先掌握)
1. 手写 Vue3 自定义 hook(如:useDebounce、useLocalStorage)
-
useDebounce(防抖 hook) :
ts// composables/useDebounce.ts import { ref, watch, unref } from 'vue' export function useDebounce<T>(value: T, delay = 300) { const debouncedValue = ref<T>(unref(value)) let timer: number watch( () => unref(value), (val) => { clearTimeout(timer) timer = setTimeout(() => { debouncedValue.value = val }, delay) }, { immediate: false } ) return debouncedValue } // 使用 <script setup> import { useDebounce } from '@/composables/useDebounce' const inputValue = ref('') const debouncedValue = useDebounce(inputValue, 500) watch(debouncedValue, (val) => { console.log('搜索:', val) // 500ms 内未输入则执行 }) </script> -
useLocalStorage(本地存储 hook) :
ts// composables/useLocalStorage.ts import { ref, watch } from 'vue' export function useLocalStorage<T>(key: string, defaultValue: T) { // 初始化 const value = ref<T>(() => { const stored = localStorage.getItem(key) return stored ? JSON.parse(stored) : defaultValue }()) // 监听变化,同步到 localStorage watch( value, (val) => { localStorage.setItem(key, JSON.stringify(val)) }, { deep: true } ) // 删除 const remove = () => { localStorage.removeItem(key) value.value = defaultValue } return { value, remove } } // 使用 <script setup> import { useLocalStorage } from '@/composables/useLocalStorage' const { value: userInfo, remove: removeUserInfo } = useLocalStorage('userInfo', { name: '', age: 0 }) // 修改 userInfo.value = { name: '张三', age: 18 } // 删除 removeUserInfo() </script>
2. 手写简易版 Vue 响应式(Vue3 Proxy 版本)
ts
// 依赖收集容器
const targetMap = new WeakMap()
let activeEffect: Function | null = null
// 收集依赖
function track(target: object, key: string | symbol) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect)
}
// 触发更新
function trigger(target: object, key: string | symbol) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
// 响应式核心
function reactive<T extends object>(target: T): T {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key) // 收集依赖
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
trigger(target, key) // 触发更新
return res
}
})
}
// 副作用函数
function effect(fn: Function) {
activeEffect = fn
fn() // 执行一次,触发 get 收集依赖
activeEffect = null
}
// 测试
const state = reactive({ count: 0 })
effect(() => {
console.log('count:', state.count) // 初始化执行,count: 0
})
state.count = 1 // 触发更新,count: 1
delete state.count // 触发更新,count: undefined
3. 手写一个通用的 Vue 分页组件(支持自定义配置、事件回调)
html
<template>
<div class="pagination" v-if="total > 0">
<button
class="page-btn"
:disabled="currentPage === 1"
@click="handlePageChange(1)"
>
首页
</button>
<button
class="page-btn"
:disabled="currentPage === 1"
@click="handlePageChange(currentPage - 1)"
>
上一页
</button>
<button
v-for="page in pageList"
:key="page"
class="page-btn"
:class="{ active: page === currentPage }"
@click="handlePageChange(page)"
>
{{ page }}
</button>
<button
class="page-btn"
:disabled="currentPage === totalPage"
@click="handlePageChange(currentPage + 1)"
>
下一页
</button>
<button
class="page-btn"
:disabled="currentPage === totalPage"
@click="handlePageChange(totalPage)"
>
尾页
</button>
<span class="page-info">
共 {{ total }} 条,每页 {{ pageSize }} 条,第 {{ currentPage }}/{{ totalPage }} 页
</span>
</div>
</template>
<script setup lang="ts">
import { computed, defineProps, defineEmits } from 'vue'
// Props
const props = defineProps<{
currentPage: number // 当前页
pageSize: number // 每页条数
total: number // 总条数
showSize?: number // 显示的页码数(默认5)
}>()
// Emits
const emits = defineEmits<{
(e: 'page-change', page: number): void
}>()
// 计算总页数
const totalPage = computed(() => Math.ceil(props.total / props.pageSize))
// 计算显示的页码列表
const pageList = computed(() => {
const showSize = props.showSize || 5
const total = totalPage.value
const current = props.currentPage
const list: number[] = []
// 边界处理
if (total <= showSize) {
for (let i = 1; i <= total; i++) list.push(i)
} else {
const half = Math.floor(showSize / 2)
let start = current - half
let end = current + half
if (start < 1) {
start = 1
end = showSize
}
if (end > total) {
end = total
start = total - showSize + 1
}
for (let i = start; i <= end; i++) list.push(i)
}
return list
})
// 页码变化
const handlePageChange = (page: number) => {
if (page < 1 || page > totalPage.value || page === props.currentPage) return
emits('page-change', page)
}
</script>
<style scoped>
.pagination {
display: flex;
align-items: center;
gap: 8px;
margin: 20px 0;
}
.page-btn {
padding: 4px 12px;
border: 1px solid #e5e7eb;
border-radius: 4px;
background: #fff;
cursor: pointer;
}
.page-btn:disabled {
cursor: not-allowed;
color: #9ca3af;
background: #f9fafb;
}
.page-btn.active {
background: #3b82f6;
color: #fff;
border-color: #3b82f6;
}
.page-info {
margin-left: 12px;
color: #6b7280;
}
</style>
4. 实现 Vue 中的防抖/节流指令(v-debounce/v-throttle)
-
v-debounce(防抖指令) :
ts// directives/debounce.ts import type { Directive } from 'vue' interface ElType extends HTMLElement { __handleClick__: Function } const debounce: Directive = { mounted(el: ElType, binding) { const { value } = binding if (typeof value !== 'function') { throw new Error('v-debounce 必须传入函数') } // 防抖参数:delay(默认300ms) const delay = binding.arg ? Number(binding.arg) : 300 let timer: number el.__handleClick__ = function () { clearTimeout(timer) timer = setTimeout(() => { value() }, delay) } el.addEventListener('click', el.__handleClick__) }, unmounted(el: ElType) { el.removeEventListener('click', el.__handleClick__) } } export default debounce -
v-throttle(节流指令) :
ts// directives/throttle.ts import type { Directive } from 'vue' interface ElType extends HTMLElement { __handleClick__: Function __isClick__: boolean } const throttle: Directive = { mounted(el: ElType, binding) { const { value } = binding if (typeof value !== 'function') { throw new Error('v-throttle 必须传入函数') } // 节流参数:interval(默认1000ms) const interval = binding.arg ? Number(binding.arg) : 1000 el.__isClick__ = true el.__handleClick__ = function () { if (!el.__isClick__) return el.__isClick__ = false value() setTimeout(() => { el.__isClick__ = true }, interval) } el.addEventListener('click', el.__handleClick__) }, unmounted(el: ElType) { el.removeEventListener('click', el.__handleClick__) } } export default throttle -
注册和使用 :
js// main.ts import { createApp } from 'vue' import debounce from './directives/debounce' import throttle from './directives/throttle' const app = createApp(App) app.directive('debounce', debounce) app.directive('throttle', throttle)html<template> <button v-debounce:500="handleSearch">防抖搜索</button> <button v-throttle:1000="handleSubmit">节流提交</button> </template>
5. 手写 Pinia 持久化插件(基于 localStorage)
ts
// plugins/piniaPersist.ts
import type { PiniaPluginContext } from 'pinia'
// 持久化插件
export function piniaPersistPlugin(context: PiniaPluginContext) {
const { store } = context
// 从 localStorage 加载数据
const persistKey = store.$id + '_persist'
const storedState = localStorage.getItem(persistKey)
if (storedState) {
store.$patch(JSON.parse(storedState))
}
// 监听 state 变化,同步到 localStorage
store.$subscribe((mutation, state) => {
localStorage.setItem(persistKey, JSON.stringify(state))
}, { deep: true })
// 提供删除方法
store.$resetPersist = () => {
localStorage.removeItem(persistKey)
store.$reset()
}
}
// 使用
// store/index.ts
import { createPinia } from 'pinia'
import { piniaPersistPlugin } from '@/plugins/piniaPersist'
const pinia = createPinia()
pinia.use(piniaPersistPlugin)
export default pinia
五、场景题(大厂/中厂重点,考察落地能力)
1. 场景1:Vue 项目首屏加载慢,你如何定位问题并优化?
- 定位问题 :
① 网络层面:Chrome DevTools → Network 面板,查看资源加载时间(如 JS/CSS/图片);
② 打包层面:webpack-bundle-analyzer/vite-bundle-analyzer分析打包体积,定位大文件;
③ 渲染层面:Chrome DevTools → Performance 面板,录制加载过程,查看瓶颈(如 JS 执行时间、DOM 渲染时间); - 优化方案 :
① 网络优化:- 路由/组件懒加载;
- CDN 引入大库(Vue、Element Plus);
- 开启 Gzip/Brotli 压缩(后端配置);
- 静态资源缓存(强缓存/协商缓存);
② 打包优化: - 按需引入第三方库;
- 拆分公共代码(splitChunks);
- 压缩代码/图片;
③ 渲染优化: - 首屏骨架屏;
- 预加载关键资源(
<link rel="preload">); - 减少首屏渲染的组件数量;
④ 接口优化: - 接口合并(减少请求数);
- 接口缓存(localStorage 缓存非实时数据);
- 服务端渲染/静态生成(SSR/SSG)。
2. 场景2:多人协作开发 Vue 项目,如何保证代码规范和质量?
- ① 代码规范 :
- 配置 ESLint + Prettier(统一代码风格,如缩进、分号、引号);
- 配置
eslint-plugin-vue(Vue 代码规范,如组件命名、props 定义); - 配置 TypeScript(类型约束,减少类型错误);
- ② 提交规范 :
- husky + lint-staged(提交前执行 ESLint 检查,不通过则禁止提交);
- commitlint(规范 commit 信息,如 feat: 新增功能、fix: 修复 bug);
- ③ 代码评审 :
- 采用 Git Flow 工作流(master/dev/feature 分支);
- 合并代码前需提交 MR/PR,至少 1 人评审通过;
- ④ 自动化测试 :
- 单元测试(Vitest/Jest)测试核心组件/工具函数;
- E2E 测试(Cypress/Playwright)测试核心业务流程;
- ⑤ 文档规范 :
- 组件文档(Storybook/VitePress);
- 接口文档(Swagger/Postman);
- 项目文档(README 说明环境搭建、启动命令)。
3. 场景3:Vue 项目中遇到内存泄漏,你如何排查和解决?
-
常见内存泄漏场景 :
① 未清除的定时器/计时器(setTimeout/setInterval);
② 未移除的事件监听(addEventListener);
③ 未取消的订阅(如 Pinia 订阅、EventBus);
④ 闭包引用(如组件销毁后,闭包仍引用组件数据);
⑤ 大型数据未释放(如长列表数据未清空); -
排查方法 :
① Chrome DevTools → Memory 面板,录制堆快照,对比组件销毁前后的内存变化;
② 查找 Detached DOM(分离的 DOM 节点),定位未释放的 DOM 引用; -
解决方法 :
① 定时器:组件卸载时(onUnmounted)清除:vue<script setup> import { onUnmounted } from 'vue' const timer = setInterval(() => {}, 1000) onUnmounted(() => { clearInterval(timer) }) </script>② 事件监听:组件卸载时移除:
js<script setup> import { onMounted, onUnmounted } from 'vue' const handleResize = () => {} onMounted(() => { window.addEventListener('resize', handleResize) }) onUnmounted(() => { window.removeEventListener('resize', handleResize) }) </script>③ 订阅/总线:组件卸载时取消:
js<script setup> import { onUnmounted } from 'vue' import { useEventBus } from '@vueuse/core' const bus = useEventBus('test') const unsubscribe = bus.on('test', () => {}) onUnmounted(() => { unsubscribe() }) </script>④ 大型数据:组件卸载时清空:
js<script setup> import { ref, onUnmounted } from 'vue' const list = ref([]) onUnmounted(() => { list.value = [] // 清空数据 }) </script>
4. 场景4:移动端 Vue 项目适配方案?
-
核心方案:vw/vh(推荐) + postcss-px-to-viewport;
-
实现步骤 :
① 安装依赖:bashnpm install postcss-px-to-viewport -D② 配置 postcss.config.js:
jsmodule.exports = { plugins: { 'postcss-px-to-viewport': { viewportWidth: 375, // 设计稿宽度(如 375px) viewportHeight: 667, // 设计稿高度 unitPrecision: 5, // 精度 viewportUnit: 'vw', // 转换单位 selectorBlackList: ['ignore'], // 忽略的选择器 minPixelValue: 1, // 最小转换像素 mediaQuery: false // 不转换媒体查询中的 px } } }③ 特殊适配:
- 字体:使用 rem(结合 rootFontSize),避免 vw 导致字体过大/过小;
- 横屏适配:监听
orientationchange事件,调整样式; - 兼容低版本浏览器:引入
viewport-units-buggyfill;
-
备选方案 :
① rem:设置根元素 font-size(如 1rem = 100px),结合 postcss-px-to-rem;
② flex 布局:弹性布局适配不同屏幕; -
注意事项 :
① 设计稿标注为 px,自动转为 vw,无需手动计算;
② 固定尺寸(如 1px 边框)需忽略转换;
③ 测试不同设备(如 iPhone 6/7/8/11/12),调整适配参数。
总结
- 基础核心:Vue2/Vue3 响应式原理、生命周期、组件通信、路由/状态管理基础是所有面试的必过项,需熟练掌握;
- 进阶重点:虚拟 DOM/Diff 算法、性能优化(打包/渲染/运行时)、工程化(Webpack/Vite)是区分中高级前端的关键;
- 架构能力:组件库设计、微前端、SSR、大型项目目录/状态拆分是大厂面试的核心,需结合实战理解落地逻辑;
- 手写代码:防抖/节流、响应式、自定义 hook、分页组件是高频考点,需手写熟练,理解核心逻辑而非死记;
- 场景题:首屏优化、内存泄漏、多人协作规范是考察落地能力的关键,需形成「定位问题→分析原因→给出方案」的答题思路。