Vue3 作为当前前端主流框架,其核心特性、底层原理及生态工具是面试中的高频考点。本文汇总了 Vue3 面试中最常被问到的 15 个问题,从响应式原理、Composition API 到生态工具,每个问题都包含「核心考点」「详细解析」和「代码示例」,帮你快速掌握面试重点,从容应对面试。

一、核心原理类

1. Vue3 响应式原理与 Vue2 有何区别?为什么用 Proxy 替代 Object.defineProperty?

核心考点:响应式底层实现的差异及 Proxy 的优势。

解析

  • Vue2 基于 Object.defineProperty 实现响应式,通过遍历对象的已有属性,为每个属性添加 getter 和 setter 拦截读写操作;
  • Vue3 改用 Proxy 实现响应式,Proxy 是 ES6 新增的「对象代理」API,可直接拦截对整个对象的操作(包括新增属性、删除属性、数组索引修改等)。

Proxy 解决的 Vue2 痛点

  1. 对象动态属性支持:Vue2 无法监听对象新增 / 删除属性(需手动 $set/$delete),而 Proxy 可直接拦截 set(新增)和 deleteProperty(删除)操作;
  2. 数组操作支持:Vue2 需重写数组 push/pop 等 7 个方法才能监听变化,且无法监听索引修改(如 arr[0] = 1),Proxy 可直接拦截数组的索引赋值和长度修改;
  3. 懒递归代理:Vue2 初始化时需递归遍历对象所有属性,性能损耗大;Vue3 仅在读取嵌套对象时才动态代理(懒加载),优化初始化性能;
  4. 拦截操作更全面:Proxy 可拦截 inkey in obj)、ownKeysObject.keys)等 13 种操作,覆盖更多场景。

代码对比

javascript

运行

// Vue2 响应式核心(简化)
function defineReactive(obj, key) {
  let value = obj[key];
  Object.defineProperty(obj, key, {
    get() { track(obj, key); return value; },
    set(newVal) { value = newVal; trigger(obj, key); }
  });
}

// Vue3 响应式核心(简化)
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) { track(target, key); return Reflect.get(target, key); },
    set(target, key, val) { Reflect.set(target, key, val); trigger(target, key); },
    deleteProperty(target, key) { Reflect.deleteProperty(target, key); trigger(target, key); }
  });
}

2. Vue3 中的 ref 和 reactive 有什么区别?分别在什么场景下使用?

核心考点:响应式 API 的使用场景及底层差异。

解析

  • ref:用于包装基础类型数据(number、string、boolean 等),返回一个带 value 属性的响应式对象;若传入对象,会自动用 reactive 包装。
  • reactive:用于包装对象 / 数组,返回一个响应式代理对象,直接操作对象属性即可触发更新(无需 .value)。

核心区别

维度refreactive
适用类型基础类型 + 对象 / 数组仅对象 / 数组
访问方式需通过 .value 访问直接访问属性
解构响应式需用 toRefs 保持响应直接解构会丢失响应式

使用场景

  • 存储基础类型(如 count: ref(0))或需要单独传递的响应式变量(如函数返回值)时用 ref
  • 存储对象 / 数组(如 user: reactive({ name: 'xxx' }))时用 reactive,避免频繁写 .value

代码示例

javascript

运行

import { ref, reactive, toRefs } from 'vue';

// ref 基础类型
const count = ref(0);
count.value++; // 需 .value 修改

// ref 对象(自动转为 reactive)
const userRef = ref({ name: 'vue3' });
userRef.value.name = 'vue3-ref'; // 需 .value 访问对象

// reactive 对象
const userReactive = reactive({ name: 'vue3' });
userReactive.name = 'vue3-reactive'; // 直接修改

// 解构 reactive 需用 toRefs 保持响应式
const { name } = toRefs(userReactive);

二、Composition API 类

3. Composition API 与 Options API 相比有什么优势?为什么说它解决了「逻辑碎片化」问题?

核心考点:两种 API 的设计理念及适用场景。

解析

  • Options API(Vue2 主流):通过 datamethodswatchcomputed 等配置项组织代码,同一功能的逻辑被拆分到不同配置中(如表单验证的「数据」在 data,「验证方法」在 methods,「监听」在 watch),即「逻辑碎片化」。
  • Composition API(Vue3 新增):通过 setup 函数或 <script setup> 按功能聚合逻辑,同一功能的「数据、方法、监听、计算属性」集中在一处,解决碎片化问题。

优势

  1. 逻辑聚合:复杂组件(如包含表单、列表、弹窗的页面)中,同一功能的代码无需跨配置项查找,维护成本低;
  2. 逻辑复用:通过「组合函数(useXxx)」复用逻辑(如 useFormuseList),替代 Vue2 的 mixins(避免命名冲突、来源模糊问题);
  3. 类型友好:与 TypeScript 结合更自然,类型推导更清晰(Options API 中 this 类型难以处理);
  4. 灵活组织:可按功能拆分代码块(甚至拆分到单独文件),大型项目更易维护。

代码对比

vue

<!-- Options API(逻辑碎片化) -->
<script>
export default {
  data() { return { count: 0 }; }, // 数据
  methods: { add() { this.count++; } }, // 方法
  watch: { count(newVal) { console.log(newVal); } } // 监听(分散)
};
</script>

<!-- Composition API(逻辑聚合) -->
<script setup>
import { ref, watch } from 'vue';
// 同一功能的逻辑集中
const count = ref(0); // 数据
const add = () => { count.value++; }; // 方法
watch(count, (newVal) => { console.log(newVal); }); // 监听
</script>

4. setup 函数的执行时机是什么?它的参数和返回值有什么作用?

核心考点setup 作为 Composition API 的入口,其生命周期和参数用法。

解析

  • 执行时机:在组件实例创建前执行,位于 beforeCreate 之前,此时组件实例尚未初始化,无法访问 thisthis 为 undefined)。
  • 参数
    • props:组件接收的 props(响应式对象,需通过 defineProps 定义类型);
    • context:包含 attrs(非响应式的属性集合)、slots(插槽内容)、emit(触发事件的函数)、expose(暴露组件内部方法给父组件)。
  • 返回值
    • 返回对象:对象中的属性和方法可在模板中直接使用(如 return { count, add },模板中可直接用 {{ count }} 和 @click="add");
    • 返回函数:若返回渲染函数(如 () => h('div', count.value)),则模板会被该函数替代。

代码示例

vue

<template>
  <div>{{ count }}</div>
  <button @click="add">+1</button>
</template>

<script>
export default {
  props: { msg: String },
  setup(props, context) {
    // 访问 props(响应式)
    console.log(props.msg);
    // 触发事件
    const emit = context.emit;
    // 定义响应式数据
    const count = ref(0);
    const add = () => { count.value++; };
    // 返回给模板
    return { count, add };
  }
};
</script>

<!-- 语法糖简化(<script setup>) -->
<script setup>
const props = defineProps({ msg: String }); // 替代 props 参数
const emit = defineEmits(['change']); // 替代 context.emit
// ... 逻辑同上
</script>

5. Vue3 生命周期钩子与 Vue2 有何区别?如何在 Composition API 中使用?

核心考点:生命周期钩子的变化及组合式 API 中的用法。

解析

  • Vue3 保留了大部分生命周期钩子,但命名更规范(去掉 before/after 中的冗余),且在 Composition API 中需手动导入使用;
  • setup 函数替代了 beforeCreate 和 created(因为 setup 在这两个钩子前执行)。

对应关系

Vue2 生命周期Vue3 组合式钩子作用说明
beforeCreate/createdsetup(直接替代)组件初始化前 / 初始化时
beforeMountonBeforeMount组件挂载前
mountedonMounted组件挂载后
beforeUpdateonBeforeUpdate数据更新,DOM 渲染前
updatedonUpdated数据更新,DOM 渲染后
beforeDestroyonBeforeUnmount组件卸载前
destroyedonUnmounted组件卸载后

使用示例

javascript

运行

import { onMounted, onUnmounted } from 'vue';

setup() {
  // 组件挂载后执行
  onMounted(() => {
    console.log('组件已挂载');
    // 绑定事件
    window.addEventListener('resize', handleResize);
  });

  // 组件卸载前执行(清理副作用)
  onUnmounted(() => {
    console.log('组件即将卸载');
    // 解绑事件
    window.removeEventListener('resize', handleResize);
  });

  const handleResize = () => { /* ... */ };
}

三、新特性类

6. Vue3 中的 Teleport 组件有什么作用?使用时需要注意什么?

核心考点Teleport 的应用场景及使用陷阱。

解析

  • Teleport(瞬移组件)用于将组件的部分内容「渲染到指定 DOM 节点」,解决嵌套组件中的样式隔离(如弹窗 z-index 层级问题)或 DOM 结构限制(如弹窗需脱离父容器滚动)。

使用场景

  • 弹窗、模态框(避免被父组件 overflow: hidden 截断);
  • 通知提示(如全局 Toast,需渲染到 body 下)。

注意事项

  1. 目标节点必须存在to 属性指定的目标节点(如 #modal-container)需在 HTML 中预先定义,否则内容会丢失;
  2. 样式隔离问题:若使用 <style scoped>,样式会因 data-v-xxx 前缀失效,需用 :deep() 穿透或使用全局样式;
  3. 事件冒泡Teleport 只改变 DOM 结构,不改变组件树层级,事件仍会向父组件冒泡(需用 @click.stop 阻止)。

代码示例

vue

<template>
  <button @click="show = true">打开弹窗</button>
  <!-- 将弹窗渲染到 body 下 -->
  <teleport to="body">
    <div v-if="show" class="modal" @click.stop>
      <div class="content" @click.stop>
        弹窗内容
        <button @click="show = false">关闭</button>
      </div>
    </div>
  </teleport>
</template>

<style scoped>
/* 用 :deep() 穿透 scoped,作用于 Teleport 内容 */
:deep(.modal) {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.5);
}
</style>

7. Suspense 组件的作用是什么?使用时需要注意哪些限制?

核心考点Suspense 的设计目的及使用边界。

解析

  • Suspense 用于优雅处理异步操作,在等待异步组件或异步数据加载时,显示 fallback 内容(如「加载中...」),替代传统的 v-if 控制加载状态。

使用条件

  • 只能包裹「异步组件」(通过 defineAsyncComponent 定义)或「返回 Promise 的组件」(如 setup 函数返回 Promise)。

限制

  1. 不处理错误Suspense 仅等待异步完成,不捕获错误,需配合 onErrorCaptured 或错误边界组件处理错误;
  2. setup 中不能直接用顶层 await<script setup> 中不允许顶层 await,需用异步组件包裹;
  3. 嵌套失效:子 Suspense 的异步操作会冒泡到最近的父 Suspense,嵌套使用可能导致 fallback 混乱。

代码示例

vue

<!-- 父组件 -->
<template>
  <Suspense>
    <template #default>
      <AsyncComponent /> <!-- 异步组件 -->
    </template>
    <template #fallback>加载中...</template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue';
// 定义异步组件(返回 Promise)
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));
</script>

<!-- AsyncComponent.vue(异步数据加载) -->
<script setup>
// 模拟异步请求
const fetchData = () => new Promise(resolve => {
  setTimeout(() => resolve({ name: '异步数据' }), 1000);
});
const data = await fetchData(); // 这里实际需用 <script setup async>(Vue3.3+ 支持)
</script>

8. Vue3 对 v-model 做了哪些重构?如何实现多字段双向绑定?

核心考点v-model 语法的变化及灵活用法。

解析:Vue2 中 v-model 本质是 :value + @input 的语法糖,且一个组件只能有一个 v-model;Vue3 对 v-model 重构后:

  1. 默认 prop 与事件变化

    • 旧:value prop + input 事件;
    • 新:modelValue prop + update:modelValue 事件(更语义化)。
  2. 支持多字段绑定:通过 v-model:字段名 实现多个双向绑定(替代 Vue2 的 .sync 修饰符)。

  3. 自定义修饰符:支持自定义 v-model 修饰符(如 v-model.capitalize),通过 xxxModifiers prop 接收。

代码示例

vue

<!-- 父组件:多字段绑定 -->
<template>
  <UserForm 
    v-model:name="username" 
    v-model:age="userAge" 
  />
</template>

<script setup>
import { ref } from 'vue';
const username = ref('');
const userAge = ref(0);
</script>

<!-- 子组件:UserForm.vue -->
<template>
  <input 
    :value="name" 
    @input="$emit('update:name', $event.target.value)"
  >
  <input 
    type="number"
    :value="age" 
    @input="$emit('update:age', $event.target.value)"
  >
</template>

<script setup>
defineProps(['name', 'age']);
defineEmits(['update:name', 'update:age']);
</script>

四、生态工具类

9. Pinia 与 Vuex 相比有什么优势?为什么说它是 Vue3 的官方推荐?

核心考点:状态管理库的迭代及设计理念变化。

解析:Pinia 是 Vue 官方推荐的状态管理库,完全替代 Vuex,其优势体现在:

  1. 移除冗余概念

    • 去掉 Vuex 的 mutations(同步操作必须用 mutations 是冗余设计),actions 可直接处理同步 / 异步逻辑;
    • 无需 modules 和 namespaced,通过多个独立 store 实现模块化(天然隔离)。
  2. 更简洁的 API

    • 定义 store 更简单(defineStore 函数);
    • 组件中直接调用 store.action(),无需 dispatch/commit
  3. TypeScript 友好

    • 天生支持类型推导,无需手动声明 StateGetters 等类型;
    • 与 Vue3 的 Composition API 无缝配合。
  4. 更好的开发体验

    • 支持 Vue Devtools 调试(包括时间旅行);
    • 支持服务器端渲染(SSR)。

代码对比

javascript

运行

// Vuex 旧代码
const store = new Vuex.Store({
  state: { count: 0 },
  mutations: { increment(state) { state.count++; } }, // 冗余
  actions: { asyncIncrement({ commit }) { setTimeout(() => commit('increment'), 1000); } }
});

// Pinia 新代码
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() { this.count++; }, // 同步
    async asyncIncrement() { setTimeout(() => this.increment(), 1000); } // 异步
  }
});

10. Vue Router 4 与 Vue Router 3 有哪些主要变化?

核心考点:路由库的升级点及用法变化。

解析:Vue Router 4 是适配 Vue3 的路由库,主要变化包括:

  1. 创建方式变化

    • 旧:new VueRouter({ routes, mode: 'history' })
    • 新:createRouter({ routes, history: createWebHistory() })(更明确的历史模式创建)。
  2. 组合式 API 支持

    • 新增 useRoute(替代 this.$route)和 useRouter(替代 this.$router),在 setup 中使用;
    • 路由守卫支持组合式写法(如 onBeforeRouteLeave)。
  3. 移除 * 通配符

    • 旧:{ path: '*', component: NotFound }
    • 新:{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }(更灵活的参数匹配)。
  4. 路由参数获取变化

    • 动态路由参数(如 :id)需通过 route.params.id 获取(与 Vue2 一致,但类型更明确)。

代码示例

javascript

运行

// 路由创建(router/index.js)
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
  history: createWebHistory(), // 替代 mode: 'history'
  routes: [
    { path: '/', component: Home },
    { path: '/user/:id', component: User },
    { path: '/:pathMatch(.*)*', component: NotFound } // 通配符
  ]
});

// 组件中使用(Composition API)
<script setup>
import { useRoute, useRouter } from 'vue-router';
const route = useRoute(); // 替代 this.$route
const router = useRouter(); // 替代 this.$router

console.log(route.params.id); // 获取动态参数
router.push('/home'); // 导航跳转
</script>

11. Vite 与 Webpack 相比有什么优势?为什么说它的热更新更快?

核心考点:构建工具的原理差异及性能优势。

解析:Vite 是 Vue 作者开发的新一代构建工具,核心优势源于其「按需编译」的设计:

  1. 开发服务器原理

    • Webpack:启动时需打包所有模块(即使未用到),生成 bundle 后启动服务器,冷启动慢;
    • Vite:基于浏览器原生 ESM,启动时不打包,直接提供源码,浏览器请求哪个模块就编译哪个,冷启动极快(中等项目从 30 秒→2 秒)。
  2. 热更新(HMR)

    • Webpack:修改模块后,需重新打包该模块及依赖链,更新成本高;
    • Vite:直接更新修改的模块,无需重新打包依赖,热更新响应时间从 500ms→50ms 以内。
  3. 编译工具

    • Webpack 用 Babel 编译 JS(JS 编写,速度慢);
    • Vite 开发时用 esbuild(Go 编写,比 Babel 快 10-100 倍)编译 JS/TS,生产环境用 Rollup 打包(优化更好)。

总结:Vite 通过「按需编译」和「高效工具链」解决了 Webpack 开发时的性能痛点,尤其适合大型项目。

五、实战与优化类

12. 如何在 Vue3 中实现逻辑复用?组合函数(useXxx)与 Vue2 的 mixins 有何区别?

核心考点:逻辑复用方案的优劣对比。

解析:Vue3 推荐用「组合函数(useXxx)」实现逻辑复用,替代 Vue2 的 mixins,核心区别如下:

维度mixins(Vue2)组合函数(Vue3)
命名冲突多个 mixins 可能有同名属性 / 方法,覆盖时难以察觉显式解构返回值,命名冲突可主动处理
逻辑来源组件中无法直观知道属性 / 方法来自哪个 mixin从函数名(如 useForm)可明确来源
参数传递需通过 this 隐式传递,依赖组件上下文可通过函数参数显式传递,更灵活

组合函数示例(封装表单逻辑):

javascript

运行

// hooks/useForm.js(组合函数)
import { ref } from 'vue';
export function useForm(initialValues) {
  const form = ref({ ...initialValues });
  const reset = () => { form.value = { ...initialValues }; };
  return { form, reset }; // 暴露数据和方法
}

// 组件中使用
<script setup>
import { useForm } from './hooks/useForm';
// 显式获取,无冲突风险
const { form, reset } = useForm({ username: '', password: '' });
</script>

13. Vue3 中如何优化大型列表的性能?

核心考点:列表渲染的性能优化方案。

解析:大型列表(如 1000+ 条数据)容易导致渲染卡顿,Vue3 中可通过以下方式优化:

  1. 虚拟滚动:只渲染可视区域内的列表项,隐藏区域的项不渲染(用 vue-virtual-scroller 等库);
  2. 固定 key:确保列表项 key 唯一且稳定(避免用索引作为 key,否则数据变化时会重新创建所有 DOM);
  3. 列表项组件缓存:用 keep-alive 缓存不常变化的列表项组件;
  4. 分页加载:将数据分页,每次只加载当前页数据(如配合 infinite-scroll 实现滚动加载);
  5. 避免不必要的响应式:非响应式数据用 shallowRef 或普通变量存储(减少响应式追踪开销)。

代码示例(虚拟滚动)

vue

<template>
  <virtual-scroller
    :items="largeList"
    :item-height="50"
    class="scroller"
  >
    <template #default="{ item }">
      <div class="item">{{ item.name }}</div>
    </template>
  </virtual-scroller>
</template>

<script setup>
import { ref } from 'vue';
import VirtualScroller from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';

// 模拟大型列表
const largeList = ref(Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` })));
</script>

14. Vue3 中的 shallowRefshallowReactivereadonly 有什么作用?

核心考点:响应式 API 的进阶用法及性能优化。

解析:这些 API 用于控制响应式的深度和可修改性,优化性能或限制数据修改:

  • shallowRef:只对 .value 进行响应式追踪,不处理 value 内部的对象(浅响应式);

    • 适用场景:存储大型非响应式对象(如 DOM 元素、第三方库实例),避免深层响应式的性能开销。
  • shallowReactive:只对对象的顶层属性进行响应式追踪,不处理嵌套对象(浅响应式);

    • 适用场景:已知嵌套对象无需响应式的场景(如配置对象),减少递归代理的性能损耗。
  • readonly:创建一个只读的响应式对象(深层只读,修改会报警告);

    • 适用场景:保护全局配置或状态,防止被意外修改(如 useStore 中的状态只读副本)。

代码示例

javascript

运行

import { shallowRef, shallowReactive, readonly } from 'vue';

// shallowRef:只追踪 .value 变化
const shallow = shallowRef({ name: 'vue3' });
shallow.value.name = 'test'; // 不触发更新
shallow.value = { name: 'test' }; // 触发更新

// shallowReactive:只追踪顶层属性
const shallowObj = shallowReactive({ a: 1, b: { c: 2 } });
shallowObj.a = 2; // 触发更新
shallowObj.b.c = 3; // 不触发更新

// readonly:只读
const original = reactive({ count: 0 });
const readOnlyObj = readonly(original);
readOnlyObj.count = 1; // 报警告,修改无效

15. Vue3 如何实现组件通信?有哪些方式?

核心考点:组件通信的多种方案及适用场景。

解析:Vue3 支持多种组件通信方式,选择取决于组件关系:

  1. 父子通信

    • 父→子:propsdefineProps 定义);
    • 子→父:emitdefineEmits 定义事件);
    • 父访问子:ref + defineExpose(子组件暴露属性 / 方法)。
  2. 祖孙 / 跨级通信

    • provide + inject(依赖注入,父组件提供数据,子孙组件注入);
    • 优势:无需手动传递 props 多层级,适合深层级通信(如主题配置、用户信息)。
  3. 任意组件通信

    • Pinia(全局状态管理,适合共享复杂状态);
    • 事件总线(mitt 库,适合简单的跨组件事件通知)。

代码示例(provide + inject)

vue

<!-- 父组件:提供数据 -->
<script setup>
import { provide } from 'vue';
// 提供主题配置
provide('theme', { color: 'blue', size: 16 });
</script>

<!-- 孙组件:注入数据 -->
<script setup>
import { inject } from 'vue';
// 注入主题配置(第二个参数为默认值)
const theme = inject('theme', { color: 'black', size: 14 });
console.log(theme.color); // 'blue'
</script>
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐