Vue 3 的面试题主要围绕组合式 API (Composition API) 、响应式原理 、性能优化 以及与 Vue 2 的区别展开。以下是高频核心考点及标准答案:
一、 核心原理类(必考)
1. Vue 3 的响应式原理是什么?与 Vue 2 有何区别?
核心答案:
- Vue 3 :使用 Proxy 实现响应式。通过创建对象的代理,拦截对象的读取(get)和设置(set)操作,实现依赖收集和派发更新。
- Vue 2 :使用 Object.defineProperty 递归遍历对象属性,通过 getter/setter 劫持数据。
区别与优势:
- 性能:Proxy 是懒代理,无需递归初始化所有属性,性能更好。
- 功能 :Proxy 能监听新增/删除属性 、数组索引/长度变化 ,无需
Vue.set/Vue.delete。 - 支持类型:Proxy 支持 Map、Set 等复杂数据结构,Vue 2 不支持。
2. Vue 3 为什么这么快?(性能优化)
核心答案:
- 响应式重构:Proxy 替代 defineProperty,减少初始化开销。
- 编译时优化 :
- 静态提升 (Hoist Static):将静态节点提升到渲染函数外,避免重复创建。
- Patch Flag:给动态节点打标记,Diff 时只对比带标记的节点,跳过静态节点。
- Tree Shaking:支持按需引入,未使用的 API 不会被打包。
二、 组合式 API (Composition API)
3. 组合式 API 与选项式 API 的区别?
核心答案:
- 代码组织 :组合式 API 按逻辑功能 组织代码(如将用户逻辑放在一起),选项式 API 按选项组织(data、methods 分离)。
- 逻辑复用 :组合式 API 通过自定义 Hook (Composables) 复用逻辑,解决了 Mixins 的命名冲突和来源不清晰问题。
- TypeScript 支持:组合式 API 天然支持更好的类型推断。
4. ref 和 reactive 的区别?
核心答案:
ref:用于包装基本类型 (String, Number),通过.value访问。也可包装对象,内部调用reactive。reactive:用于包装对象/数组,返回代理对象,直接访问属性。- 选择原则 :基本类型用
ref,对象/数组用reactive。
5. watch 和 watchEffect 的区别?
核心答案:
watch:需要明确指定监听的数据源,支持获取旧值和新值,适合精确监听。watchEffect:自动收集回调函数内的依赖,立即执行一次,适合依赖动态变化的场景。
三、 生命周期与新特性
6. Vue 3 生命周期有哪些变化?
核心答案:
- 重命名 :
beforeDestroy→beforeUnmount,destroyed→unmounted。 - 组合式写法 :使用
onMounted、onUnmounted等函数。 setup替代 :setup函数执行时机相当于 Vue 2 的beforeCreate和created,这两个钩子在 Vue 3 中不再推荐使用。
7. Vue 3 有哪些新特性?
核心答案:
- Fragment:支持多根节点模板。
- Teleport:将组件渲染到 DOM 的指定位置(如 body 下的弹窗)。
- Suspense:处理异步组件的加载状态。
<script setup>:编译时语法糖,简化组合式 API 的写法。
四、 实战与场景题
8. 如何实现逻辑复用?
核心答案 :
使用组合式函数 (Composables) 。将逻辑封装为函数,返回响应式数据和方法,在组件中引入使用。例如封装 useCounter 管理计数逻辑。
9. 如何优化 Vue 3 应用的性能?
核心答案:
- 编译层:利用静态提升和 Patch Flag。
- 代码层 :使用
shallowRef/shallowReactive避免深层响应式代理;使用v-memo缓存模板片段。 - 工程层 :组件懒加载(
defineAsyncComponent),路由懒加载。
五、 加分项(进阶)
- Diff 算法优化:Vue 3 引入 Block Tree 和 Patch Flag,只追踪动态节点,减少 Diff 范围。
- Tree Shaking 原理:Vue 3 源码采用 ES Module 模块化,打包工具(如 Webpack)能识别未使用的导出并剔除。
Vue 3 的组件传值方式多样,主要分为父子通信 、跨级/多级通信 和全局状态三大类。以下是所有方式的总结与对比:
一、 父子组件通信(最常用)
1. Props / Emits(单向数据流)
-
父传子 (Props):
vue// 父组件 <template> <Child :title="msg" :count="10" /> </template> <script setup> import { ref } from 'vue'; const msg = ref('Hello'); </script>vue// 子组件 <script setup> // 接收 Props const props = defineProps({ title: String, count: { type: Number, default: 0 } }); </script> -
子传父 (Emits):
vue// 子组件 <template> <button @click="handleClick">传值</button> </template> <script setup> const emit = defineEmits(['update']); const handleClick = () => { emit('update', { data: '子组件的数据' }); }; </script>vue// 父组件 <template> <Child @update="handleUpdate" /> </template> <script setup> const handleUpdate = (value) => { console.log(value); // { data: '子组件的数据' } }; </script>
2. v-model 双向绑定(语法糖,用于表单组件)
-
父组件 :
vue<template> <Child v-model="name" /> </template> <script setup> const name = ref(''); </script> -
子组件 :
vue<template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template> <script setup> const props = defineProps(['modelValue']); const emit = defineEmits(['update:modelValue']); </script>
3. ref 和 defineExpose(获取子组件实例)
-
父组件 :
vue<template> <Child ref="childRef" /> </template> <script setup> import { ref, onMounted } from 'vue'; const childRef = ref(null); onMounted(() => { // 调用子组件暴露的方法 childRef.value.someMethod(); }); </script> -
子组件 :
vue<script setup> const someMethod = () => { console.log('子组件方法'); }; // 暴露给父组件 defineExpose({ someMethod }); </script>
二、 跨级/兄弟组件通信
4. Provide / Inject(依赖注入,解决多级嵌套传值)
-
祖先组件 (Provide) :
vue<script setup> import { provide, ref } from 'vue'; const count = ref(0); // 提供响应式数据 provide('countKey', count); </script> -
后代组件 (Inject) :
vue<script setup> import { inject } from 'vue'; const count = inject('countKey'); </script>
5. 事件总线 (Event Bus)(已不推荐)
Vue 3 取消了 $on、$off 等 API,建议使用第三方库(如 mitt)替代:
javascript
import mitt from 'mitt';
const emitter = mitt();
// 发送
emitter.emit('update', data);
// 接收
emitter.on('update', (data) => { console.log(data); });
三、 全局状态管理
6. Pinia(官方推荐)
-
定义 Store (store/user.js) :
javascriptimport { defineStore } from 'pinia'; export const useUserStore = defineStore('user', { state: () => ({ name: '张三' }), actions: { updateName(newName) { this.name = newName; } } }); -
组件中使用 :
vue<template> <div>{{ userStore.name }}</div> </template> <script setup> import { useUserStore } from '@/stores/user'; const userStore = useUserStore(); // 修改状态 userStore.updateName('李四'); </script>
7. Vuex 4(Vue 3 兼容版)
- 仍可使用,但官方已推荐迁移至 Pinia。
四、 总结与选择建议
| 场景 | 推荐方式 | 说明 |
|---|---|---|
| 父子组件 | Props / Emits, v-model | 简单、清晰,符合单向数据流 |
| 获取子组件实例 | ref + defineExpose | 父组件需要调用子组件方法时使用 |
| 多级嵌套 | Provide / Inject | 避免逐层传递,适合主题、配置等 |
| 兄弟组件 / 无关联组件 | Pinia | 全局状态管理,替代 Vuex |
| 简单事件通信 | mitt 等事件库 | 小型项目快速实现事件通信 |
最佳实践:
- 优先使用 Props / Emits 处理父子通信。
- 复杂应用的状态管理首选 Pinia。
- 主题、用户信息等全局数据使用 Provide / Inject 或 Pinia。
Vue 3 的路由守卫与 Vue 2 类似,但需在 Vue Router 4 中使用。路由守卫主要用于控制导航权限,分为全局守卫 、路由独享守卫 和组件内守卫三类。
一、 全局守卫(在路由实例上定义)
1. 全局前置守卫 beforeEach(最常用)
-
执行时机 :路由切换前,常用于登录验证、权限检查。
-
使用示例 :
javascriptimport { createRouter, createWebHistory } from 'vue-router'; const router = createRouter({ /* ... */ }); router.beforeEach((to, from, next) => { // 1. 检查是否需要登录 if (to.meta.requiresAuth && !isLoggedIn()) { // 未登录,跳转到登录页 next({ path: '/login', query: { redirect: to.fullPath } }); } else if (to.path === '/login' && isLoggedIn()) { // 已登录却访问登录页,重定向到首页 next({ path: '/' }); } else { // 放行 next(); } });
2. 全局解析守卫 beforeResolve
- 执行时机 :在导航被确认前 ,所有组件内守卫和异步路由组件被解析之后。适合处理需要确保数据加载完成的场景。
3. 全局后置守卫 afterEach
-
执行时机 :路由切换后,常用于日志记录、页面标题设置。
-
无
next函数 :javascriptrouter.afterEach((to, from) => { // 设置页面标题 document.title = to.meta.title || '默认标题'; // 发送页面访问统计 logAnalytics(to.fullPath); });
二、 路由独享守卫(在路由配置中定义)
在单个路由的配置对象中使用 beforeEnter,只对该路由生效。
javascript
const routes = [
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true },
beforeEnter: (to, from, next) => {
// 检查用户权限
if (!hasPermission('admin')) {
next({ path: '/403' }); // 无权限,跳转到403页
} else {
next(); // 放行
}
}
}
];
三、 组件内守卫(在组件中定义)
1. beforeRouteEnter
-
执行时机 :进入组件前 ,此时组件实例尚未创建。
-
特殊用法 :可以通过
next(vm => {})访问组件实例。javascript<script setup> import { onBeforeRouteEnter } from 'vue-router'; onBeforeRouteEnter((to, from, next) => { // 无法访问 this next(vm => { // 通过 vm 访问组件实例 console.log(vm.someData); }); }); </script>
2. beforeRouteUpdate
-
执行时机 :当前路由改变,但组件被复用时 (如从
/user/1跳转到/user/2)。 -
用法 :
javascript<script setup> import { onBeforeRouteUpdate } from 'vue-router'; onBeforeRouteUpdate((to, from, next) => { // 重新获取数据 fetchUserData(to.params.id); next(); }); </script>
3. beforeRouteLeave
-
执行时机 :离开当前组件对应路由前,常用于防止未保存离开。
-
用法 :
javascript<script setup> import { onBeforeRouteLeave } from 'vue-router'; onBeforeRouteLeave((to, from, next) => { if (formHasUnsavedChanges.value) { const confirmLeave = window.confirm('有未保存的更改,确定离开吗?'); if (confirmLeave) { next(); // 确认离开 } else { next(false); // 取消导航 } } else { next(); // 直接离开 } }); </script>
四、 组合式 API 写法
在 <script setup> 中,需从 vue-router 导入对应的组合式函数:
vue
<script setup>
import { onBeforeRouteEnter, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router';
// 组件内守卫
onBeforeRouteEnter((to, from, next) => { /* ... */ });
onBeforeRouteUpdate((to, from, next) => { /* ... */ });
onBeforeRouteLeave((to, from, next) => { /* ... */ });
</script>
五、 执行顺序总结
完整的导航解析流程如下:
- 触发导航 :调用
router.push()或点击<router-link>。 - 调用失活组件的
beforeRouteLeave。 - 调用全局的
beforeEach。 - 调用重用组件的
beforeRouteUpdate(如果组件被复用)。 - 调用路由配置中的
beforeEnter。 - 解析异步路由组件。
- 调用被激活组件的
beforeRouteEnter。 - 调用全局的
beforeResolve。 - 导航被确认。
- 调用全局的
afterEach。 - 触发 DOM 更新。
六、 最佳实践
- 权限校验 :在
beforeEach中统一处理登录状态和路由权限。 - 数据预取 :在
beforeRouteEnter或beforeResolve中获取必要数据。 - 离开确认 :在
beforeRouteLeave中提示用户保存未提交的数据。 - 组合式 API 优先 :新项目使用组合式 API 的守卫函数,与
<script setup>风格保持一致。
以下是 Vue 3 的核心原理面试题,涵盖了响应式、虚拟DOM、编译优化、生命周期等多个维度的高频考点:
一、 响应式原理
1. Vue 3 的响应式原理是什么?(与 Vue 2 对比)
-
核心机制 :Vue 3 使用 ES6 的 Proxy 代理对象,拦截对象的读取和写入操作。依赖收集和触发更新在拦截器中自动完成。
-
对比 Vue 2:
Vue 2 Vue 3 使用 Object.defineProperty 劫持 使用 Proxy 代理 递归遍历所有属性,初始化性能差 惰性代理,访问时递归 无法检测新增/删除属性 可直接检测 无法原生支持 Map/Set 等 原生支持 数组需重写 7 个方法 可直接检测索引变化 -
代码示例原理:
javascript// 简化版原理 function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { track(target, key); // 收集依赖 return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); trigger(target, key); // 触发更新 return result; } }); }
2. ref 和 reactive 的实现原理区别?
ref:内部用class RefImpl包装。如果是基本类型,通过.value的 getter/setter 劫持(类似 Vue 2);如果是对象,内部转为reactive代理。reactive:直接使用 Proxy 代理整个对象。
3. 依赖收集(track)和派发更新(trigger)如何工作?
- 依赖收集 :在
effect(副作用函数,如组件的render函数)执行时,触发get拦截,将当前effect存入一个"桶"(target -> key -> effect的 Map 结构)。 - 派发更新 :当数据变化触发
set时,从"桶"中取出对应的effect重新执行。
二、 虚拟 DOM 与 Diff 算法
4. Vue 3 的虚拟 DOM 有什么优化?
- 静态提升 (Static Hoisting):编译时将静态节点提取到渲染函数外部,避免重复创建。
- Patch Flag :为动态节点添加标记(如
1表示文本动态),Diff 时只对比带标记的节点。 - 区块树 (Block Tree):将模板分为静态区和动态区,减少递归遍历深度。
5. Diff 算法的优化点?
Vue 3 引入 最长递增子序列 (LIS) 算法优化对比效率:
- 同层级对比:与 Vue 2 一致,只对比同级节点。
- Key 的重要性:Key 是复用节点的唯一标识,避免就地复用导致的状态错误。
- 优化流程 :
- 预处理:跳过前置和后置的相同节点。
- 如果新旧节点顺序变化,通过 LIS 找到最长稳定序列,最小化移动次数。
三、 编译优化
6. Tree Shaking 是如何实现的?
Vue 3 源码采用 ES Module 模块化。打包工具(如 Webpack、Vite)在打包时会分析导入导出,移除未被使用的模块。例如,如果不使用 transition 组件,则最终打包产物不会包含相关代码。
7. 静态节点提升 (Static Hoisting) 如何提升性能?
- 编译前:模板中的静态节点在每次渲染时都会重新创建 VNode。
- 编译后:静态节点被提取为常量,在渲染函数外创建一次,后续渲染直接复用。
四、 组合式 API 原理
8. 为什么组合式 API 解决了 Mixins 的问题?
- 命名冲突:Mixins 合并时同名属性/方法会被覆盖,组合式 API 通过函数作用域隔离。
- 来源不清晰:Mixins 的属性和方法来源不明确,组合式 API 的变量显示导入。
- 逻辑复用:组合式函数可按功能划分,更灵活。
9. <script setup> 编译时做了什么?
- 将模板编译为
render函数。 - 自动将顶层变量、函数暴露给模板,无需
return。 - 自动处理
defineProps、defineEmits等编译宏。
五、 生命周期与渲染流程
10. Vue 3 的生命周期有哪些变化?
- 重命名:
beforeDestroy→beforeUnmount,destroyed→unmounted。 - 新增:
renderTracked(调试响应式依赖收集)、renderTriggered(调试响应式触发更新)。 setup替代了beforeCreate和created。
11. 响应式数据变化到视图更新的全过程?
- 数据变更 :修改响应式数据,触发
set拦截。 - 触发 effect :从依赖"桶"中找到关联的
effect(组件的渲染函数或watch)。 - 调度更新 :将
effect推入微任务队列(避免同步重复执行)。 - 执行渲染 :下一个事件循环执行
effect,生成新的虚拟 DOM。 - Diff 对比:新旧 VNode 对比,计算最小变更。
- DOM 更新:将变更应用到真实 DOM。
六、 高级原理
12. 如何实现自定义渲染器 (Custom Renderer)?
Vue 3 将平台相关的 DOM 操作抽象为渲染器接口 。通过 createRenderer 传入自定义的节点操作函数,可实现渲染到 Canvas、小程序等非 DOM 环境。
13. Teleport 和 Suspense 的实现原理?
- Teleport :编译时将
<Teleport>的内容单独提取,在目标容器中渲染,通过 Portal 技术实现挂载到任意 DOM 节点。 - Suspense :内部包装异步组件,通过
Promise链跟踪加载状态,协调多个异步依赖的加载状态。
七、 实战原理题
14. 为什么 Vue 3 的 Proxy 比 defineProperty 性能更好?
- 初始化性能:Proxy 是惰性代理,只在访问时递归;defineProperty 需递归遍历所有属性。
- 内存占用:Proxy 只需一层代理,defineProperty 需为每个属性创建闭包存储依赖。
15. 如何监听数组变化?
Vue 3 的 Proxy 可直接监听数组的索引变更和 length 变化,无需像 Vue 2 那样重写数组方法。
面试技巧:
- 回答原理题时,结合使用场景 和性能对比阐述。
- 提到优化时,务必说出具体技术名词(如 Patch Flag、静态提升)。
- 如果被追问细节,可从源码角度简述核心类(如
ReactiveEffect、ref的RefImpl)。
Vue 3 的生命周期分为组合式 API 和选项式 API 两种写法,功能完全一样,只是写法不同。Vue 3 在 Vue 2 的基础上进行了优化和重命名,使逻辑更清晰。
一、 组合式 API 的生命周期(<script setup> 或 setup() 中)
| 生命周期钩子 | 执行时机 | 使用方式 | 对应选项式 API 钩子 |
|---|---|---|---|
onBeforeMount |
组件挂载到 DOM 前 | onBeforeMount(callback) |
beforeMount |
onMounted |
组件挂载到 DOM 后 | onMounted(callback) |
mounted |
onBeforeUpdate |
数据变化,DOM 更新前 | onBeforeUpdate(callback) |
beforeUpdate |
onUpdated |
数据变化,DOM 更新后 | onUpdated(callback) |
updated |
onBeforeUnmount |
组件卸载前 | onBeforeUnmount(callback) |
beforeUnmount |
onUnmounted |
组件卸载后 | onUnmounted(callback) |
unmounted |
onErrorCaptured |
捕获后代组件错误 | onErrorCaptured(callback) |
errorCaptured |
onActivated |
<KeepAlive> 缓存组件激活时 |
onActivated(callback) |
activated |
onDeactivated |
<KeepAlive> 缓存组件失活时 |
onDeactivated(callback) |
deactivated |
onRenderTracked |
开发模式,响应式依赖被收集时 | onRenderTracked(callback) |
无,Vue 3 新增 |
onRenderTriggered |
开发模式,响应式依赖触发重新渲染时 | onRenderTriggered(callback) |
无,Vue 3 新增 |
onServerPrefetch |
服务端渲染,组件实例在服务器上被渲染前 | onServerPrefetch(callback) |
serverPrefetch |
示例用法:
vue
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
const count = ref(0);
onMounted(() => {
console.log('组件已挂载');
// 初始化操作,如调用接口、监听事件
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
console.log('组件即将卸载');
// 清理操作,如移除事件监听器、清除定时器
window.removeEventListener('resize', handleResize);
});
</script>
二、 选项式 API 的生命周期(与 Vue 2 类似但有变化)
| 生命周期钩子 | 执行时机 | 变化说明 |
|---|---|---|
beforeCreate |
实例初始化后,数据观测前 | 不推荐,用 setup 替代 |
created |
实例创建完成,数据观测已建立 | 不推荐,用 setup 替代 |
beforeMount |
挂载开始之前 | 不变 |
mounted |
实例挂载完成后 | 不变 |
beforeUpdate |
数据更新,DOM 打补丁前 | 不变 |
updated |
数据更新,DOM 打补丁后 | 不变 |
beforeUnmount |
实例销毁前 | 重命名 (Vue 2 是 beforeDestroy) |
unmounted |
实例销毁后 | 重命名 (Vue 2 是 destroyed) |
errorCaptured |
捕获后代组件错误 | 不变 |
activated |
<KeepAlive> 缓存组件激活时 |
不变 |
deactivated |
<KeepAlive> 缓存组件失活时 |
不变 |
示例用法:
vue
<script>
export default {
data() {
return { count: 0 };
},
mounted() {
console.log('组件已挂载');
},
beforeUnmount() {
console.log('组件即将卸载');
}
};
</script>
三、 生命周期执行顺序
组件创建阶段:
setup()执行(组合式 API 的开始)beforeCreate(选项式 API,不推荐)created(选项式 API,不推荐)beforeMountonBeforeMountmounted/onMounted
更新阶段(数据变化时):
beforeUpdateonBeforeUpdateupdatedonUpdated
卸载阶段:
beforeUnmountonBeforeUnmountunmountedonUnmounted
四、 重要变化说明
beforeCreate和created被替代 :在组合式 API 中,setup()函数在这两个钩子之前执行,所以应直接在setup()中执行初始化逻辑,无需使用这两个钩子。- 钩子重命名 :
beforeDestroy→beforeUnmount,destroyed→unmounted,命名更准确(销毁的是组件实例,卸载的是 DOM)。 - 新增调试钩子 :
onRenderTracked和onRenderTriggered仅用于开发模式,帮助调试响应式依赖。
五、 最佳实践
- 组合式 API 优先 :新项目推荐使用组合式 API 的生命周期函数,与
<script setup>搭配更简洁。 - 避免混用:不要在同一个组件中混用两种 API 写法。
- 异步操作位置 :
- 数据请求放在
onMounted或onCreated(选项式)中。 - DOM 操作必须放在
onMounted之后。 - 清理操作(如事件监听、定时器)放在
onBeforeUnmount中。
- 数据请求放在