Vue 2026 面试题汇总
聚焦 Vue 3.5/3.6 最新特性,涵盖响应式系统、Composition API、Vapor Mode、编译优化、状态管理等核心知识点。排除 Vue 2 Options API 过时内容。
目录
基础概念
1: Vue 3 相比 Vue 2 有哪些重大改进?
答案要点:
- 性能提升:编译优化(静态提升、Patch Flag)、响应式系统重构(Proxy)、虚拟 DOM 优化
- 体积优化:Tree-shaking 友好,按需引入,核心库体积减少 41%
- Composition API:更好的逻辑复用和代码组织方式
- TypeScript 支持:完全用 TypeScript 重写,类型推导更强
- 新特性:Teleport、Suspense、Fragment、多根节点
- 响应式系统:使用 Proxy 替代 Object.defineProperty,支持数组索引和对象新增属性
追问:为什么 Vue 3 要引入 Composition API?
2: Vue 3 的核心设计理念是什么?
答案要点:
- 渐进式框架:核心库只关注视图层,易于集成其他库或项目
- 响应式数据驱动:数据变化自动更新视图
- 组件化开发:将界面拆分为可复用的独立组件
- 编译时优化:通过编译器分析模板,生成优化的渲染代码
- 运行时 + 编译时:平衡灵活性和性能
追问:Vue 和 React 在设计理念上有什么不同?
3: 什么是 SFC(单文件组件)?它有什么优势?
答案要点:
- 定义:将模板、脚本、样式封装在一个
.vue文件中 - 优势:
- 关注点分离但保持内聚
- 支持预处理器(TypeScript、Sass、Pug)
- 作用域样式(scoped CSS)
- 编译时优化(静态分析)
- 更好的 IDE 支持
代码示例:
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
<style scoped>
button {
color: red;
}
</style>追问:<script setup> 相比普通 <script> 有什么优势?
响应式系统
4: Vue 3 的响应式原理是什么?
答案要点:
- 核心:使用 ES6 Proxy 拦截对象的读写操作
- 依赖收集:在
get陷阱中通过track()收集依赖(effect) - 触发更新:在
set陷阱中通过trigger()触发依赖执行 - 数据结构:
WeakMap<target, Map<key, Set<effect>>>存储依赖关系
简化实现:
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key) // 收集依赖
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key) // 触发更新
return true
}
})
}追问:为什么使用 WeakMap 而不是 Map?
5: reactive 和 ref 有什么区别?如何选择?
答案要点:
reactive:
- 用于对象/数组
- 返回 Proxy 对象
- 解构会失去响应式
- 不能替换整个对象
ref:
- 用于基本类型(也可用于对象)
- 通过
.value访问 - 模板中自动解包
- 可以替换整个值
选择建议:
- 基本类型用
ref - 对象/数组优先用
reactive,需要替换整个对象时用ref - 组合式函数返回值用
ref,便于解构
代码示例:
// reactive - 对象
const state = reactive({ count: 0, name: 'Vue' })
state.count++ // ✅ 响应式
const { count } = state // ❌ 失去响应式
// ref - 基本类型
const count = ref(0)
count.value++ // ✅ 响应式
// ref - 对象(可替换)
const user = ref({ name: 'Alice' })
user.value = { name: 'Bob' } // ✅ 响应式追问:如何让 reactive 对象解构后仍保持响应式?
6: toRef、toRefs、toValue 的作用和区别?
答案要点:
- toRef:为响应式对象的单个属性创建 ref,保持响应式连接
- toRefs:将响应式对象的所有属性转换为 ref,用于解构
- toValue:获取 ref/getter/普通值的实际值(Vue 3.3+)
代码示例:
const state = reactive({ count: 0, name: 'Vue' })
// toRef - 单个属性
const count = toRef(state, 'count')
count.value++ // state.count 也会变化
// toRefs - 所有属性
const { count, name } = toRefs(state)
count.value++ // state.count 也会变化
// toValue - 规范化值
function useCount(maybeRef: Ref<number> | (() => number) | number) {
const value = toValue(maybeRef) // 统一获取值
}追问:toRef 和 ref 有什么区别?
7: computed 和 watch 的区别和使用场景?
答案要点:
computed:
- 计算属性,有缓存
- 依赖的响应式数据变化时才重新计算
- 必须有返回值
- 适合:派生状态、数据转换
watch:
- 侦听器,无缓存
- 监听数据变化执行副作用
- 可以异步操作
- 适合:异步请求、DOM 操作、本地存储
代码示例:
// computed - 派生状态
const count = ref(0)
const double = computed(() => count.value * 2)
// watch - 副作用
watch(count, async (newVal) => {
const data = await fetch(`/api/${newVal}`)
// 处理数据
})
// watchEffect - 自动收集依赖
watchEffect(() => {
console.log(count.value) // 自动追踪
})追问:watchEffect 和 watch 有什么区别?
8: Vue 3 如何处理数组的响应式?
答案要点:
Proxy 优势:可以拦截数组索引和 length 的变化
支持的操作:
- 索引赋值:
arr[0] = 'new' - length 修改:
arr.length = 0 - 数组方法:push、pop、shift、unshift、splice、sort、reverse
- 索引赋值:
优化:Vue 3.5 对大型深层响应式数组优化,性能提升 10 倍
代码示例:
const arr = reactive([1, 2, 3])
// Vue 2 中不响应,Vue 3 中响应
arr[0] = 100 // ✅ 响应式
arr.length = 0 // ✅ 响应式
// 数组方法都是响应式的
arr.push(4) // ✅ 响应式
arr.splice(0, 1) // ✅ 响应式追问:为什么 Vue 2 不能检测数组索引变化?
Composition API
9: 什么是 Composition API?它解决了什么问题?
答案要点:
定义:基于函数的 API,用于组织组件逻辑
解决的问题:
- 逻辑复用困难:Mixins 命名冲突、来源不清
- 代码组织混乱:Options API 按选项分散逻辑
- 类型推导弱:this 的类型推导困难
- 大组件难维护:相关逻辑分散在不同选项中
优势:
- 更好的逻辑复用(组合式函数)
- 更灵活的代码组织
- 更好的 TypeScript 支持
- 更小的生产包体积(Tree-shaking)
追问:Composition API 会替代 Options API 吗?
10: <script setup> 有什么特点和优势?
答案要点:
特点:
- 顶层绑定自动暴露给模板
- 自动注册组件
- 更简洁的语法
- 更好的运行时性能
优势:
- 减少样板代码
- 更好的 IDE 支持
- 编译时优化(静态分析)
- TypeScript 类型推导更强
代码对比:
<!-- 普通 script -->
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const count = ref(0)
return { count }
}
})
</script>
<!-- script setup -->
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0) // 自动暴露
</script>追问:如何在 <script setup> 中定义组件名称?
11: 如何编写可复用的组合式函数(Composables)?
答案要点:
- 命名规范:use 开头(如
useMouse、useFetch) - 返回值:返回 ref 对象,便于解构
- 参数规范化:使用
toValue()或MaybeRef类型 - 副作用清理:使用
onUnmounted或onWatcherCleanup
代码示例:
// useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// 使用
const { x, y } = useMouse()追问:组合式函数和 React Hooks 有什么区别?
12: defineProps 和 defineEmits 如何使用?
答案要点:
- defineProps:定义组件接收的 props
- defineEmits:定义组件触发的事件
- 特点:编译器宏,无需导入,仅在
<script setup>中可用
Vue 3.5 新特性 - 响应式 Props 解构:
<script setup lang="ts">
// Vue 3.5+ 支持解构默认值
const { count = 0, msg = 'hello' } = defineProps<{
count?: number
msg?: string
}>()
// 解构的变量是响应式的
watchEffect(() => {
console.log(count) // 自动追踪
})
// 传递给 composable 需要包装为 getter
useDynamicCount(() => count)
// 定义事件
const emit = defineEmits<{
update: [value: number]
delete: []
}>()
emit('update', count + 1)
</script>追问:Props 解构后如何保持响应式?
组件通信
13: Vue 3 有哪些组件通信方式?
答案要点:
- Props / Emits:父子组件通信(单向数据流)
- v-model:双向绑定(语法糖)
- provide / inject:跨层级依赖注入
- defineExpose:暴露组件内部方法/属性
- Teleport:将内容渲染到 DOM 其他位置
- 状态管理:Pinia(推荐)、Vuex
代码示例:
<!-- 父组件 -->
<script setup>
import { ref, provide } from 'vue'
const theme = ref('dark')
provide('theme', theme) // 提供数据
</script>
<template>
<Child v-model="count" @update="handleUpdate" />
</template>
<!-- 子组件 -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme') // 注入数据
const modelValue = defineModel<number>() // Vue 3.4+
modelValue.value++ // 自动触发 update:modelValue
</script>追问:provide/inject 如何保持响应式?
14: defineModel 是什么?如何使用?(Vue 3.4+)
答案要点:
- 定义:简化 v-model 实现的编译器宏
- 优势:
- 自动生成 props 和 emits
- 返回可修改的 ref
- 支持多个 v-model
- 支持修饰符
代码示例:
<!-- 子组件 -->
<script setup>
// 自动声明 modelValue prop 和 update:modelValue emit
const model = defineModel<string>()
// 直接修改会自动触发 emit
model.value = 'new value'
// 多个 v-model
const title = defineModel<string>('title')
const content = defineModel<string>('content')
</script>
<template>
<input v-model="model" />
</template>
<!-- 父组件 -->
<template>
<Child v-model="text" v-model:title="title" />
</template>追问:defineModel 的实现原理是什么?
生命周期
15: Vue 3 的生命周期钩子有哪些?
答案要点:
Composition API 钩子(在 setup 中使用):
onBeforeMount:挂载前onMounted:挂载后(可访问 DOM)onBeforeUpdate:更新前onUpdated:更新后onBeforeUnmount:卸载前onUnmounted:卸载后onErrorCaptured:捕获子组件错误onActivated:keep-alive 激活onDeactivated:keep-alive 停用
注意:
setup()本身相当于beforeCreate和created- 钩子可以多次调用
- 钩子在同步执行期间调用才有效
代码示例:
import { onMounted, onUnmounted } from 'vue'
// 可以多次调用
onMounted(() => {
console.log('mounted 1')
})
onMounted(() => {
console.log('mounted 2')
})
// 清理副作用
onUnmounted(() => {
// 清理定时器、事件监听等
})追问:为什么 Vue 3 移除了 beforeCreate 和 created?
16: 父子组件的生命周期执行顺序是什么?
答案要点:
挂载阶段:
- 父 beforeMount
- 子 beforeMount
- 子 mounted
- 父 mounted
更新阶段:
- 父 beforeUpdate
- 子 beforeUpdate
- 子 updated
- 父 updated
卸载阶段:
- 父 beforeUnmount
- 子 beforeUnmount
- 子 unmounted
- 父 unmounted
原则:子组件先完成,父组件后完成
追问:为什么是这个顺序?
虚拟 DOM 与 Diff 算法
17: 什么是虚拟 DOM?为什么需要它?
答案要点:
定义:用 JavaScript 对象描述 DOM 结构
优势:
- 跨平台:可渲染到不同平台(Web、Native、Canvas)
- 性能优化:批量更新、减少 DOM 操作
- 开发体验:声明式编程,无需手动操作 DOM
劣势:
- 内存开销(维护虚拟 DOM 树)
- 首次渲染慢于直接操作 DOM
VNode 结构:
const vnode = {
type: 'div',
props: { id: 'app', class: 'container' },
children: [
{ type: 'p', children: 'Hello' }
]
}追问:虚拟 DOM 一定比真实 DOM 快吗?
18: Vue 的 Diff 算法原理是什么?
答案要点:
- 同层比较:只比较同一层级,时间复杂度 O(n)
- 双端比较:新旧列表各两个指针(头尾),4 种比较策略
- key 的作用:快速定位可复用节点,避免原地复用
- 最长递增子序列:Vue 3 优化,最小化 DOM 移动
比较策略:
- 旧头 vs 新头
- 旧尾 vs 新尾
- 旧头 vs 新尾
- 旧尾 vs 新头
- 通过 key 查找
代码示例:
<!-- ❌ 不使用 key -->
<div v-for="item in list">{{ item }}</div>
<!-- ✅ 使用唯一 key -->
<div v-for="item in list" :key="item.id">{{ item }}</div>追问:为什么不建议用 index 作为 key?
19: Vue 3 的 Diff 算法相比 Vue 2 有什么优化?
答案要点:
- 静态标记(Patch Flag):标记动态节点,跳过静态节点
- 静态提升:静态节点提升到渲染函数外,只创建一次
- Block Tree:将动态节点收集到数组,扁平化 Diff
- 最长递增子序列:优化节点移动算法
性能提升:
- 更新性能提升 1.3~2 倍
- SSR 性能提升 2~3 倍
追问:什么是 Block Tree?
编译优化
20: Vue 3 的编译器做了哪些优化?
答案要点:
1. 静态提升(Static Hoisting):
- 静态节点提升到渲染函数外
- 静态 props 提升为常量
2. Patch Flag:
- 标记动态节点类型(TEXT、CLASS、STYLE、PROPS)
- Diff 时只比较标记的部分
3. Block Tree:
- 收集动态节点到数组
- 扁平化 Diff,跳过静态节点
4. 缓存事件处理器:
- 自动缓存内联事件处理器
代码示例:
<template>
<div>
<p>Static text</p> <!-- 静态提升 -->
<p>{{ dynamic }}</p> <!-- Patch Flag: TEXT -->
<p :class="cls">Text</p> <!-- Patch Flag: CLASS -->
</div>
</template>编译后:
// 静态提升
const _hoisted_1 = createElementVNode("p", null, "Static text")
function render() {
return createElementBlock("div", null, [
_hoisted_1, // 复用
createElementVNode("p", null, _ctx.dynamic, 1 /* TEXT */),
createElementVNode("p", { class: _ctx.cls }, "Text", 2 /* CLASS */)
])
}追问:如何查看编译后的代码?
21: 什么是 Patch Flag?有哪些类型?
答案要点:
- 定义:标记 VNode 的动态内容类型,优化 Diff
- 常见类型:
1 (TEXT):动态文本2 (CLASS):动态 class4 (STYLE):动态 style8 (PROPS):动态属性16 (FULL_PROPS):有动态 key 的属性32 (HYDRATE_EVENTS):事件监听器64 (STABLE_FRAGMENT):稳定的 Fragment128 (KEYED_FRAGMENT):有 key 的 Fragment256 (UNKEYED_FRAGMENT):无 key 的 Fragment-1 (HOISTED):静态节点-2 (BAIL):Diff 算法应该退出优化模式
作用:Diff 时只检查对应类型的变化
追问:如何禁用编译优化?
Vue 3.5 3.6 新特性
22: Vue 3.5 有哪些重要新特性?
答案要点:
1. 响应式系统优化:
- 内存使用减少 56%
- 大型深层数组性能提升 10 倍
2. 响应式 Props 解构:
- 支持解构默认值
- 解构变量保持响应式
3. SSR 改进:
- Lazy Hydration(懒水合)
useId():生成稳定的唯一 IDdata-allow-mismatch:允许特定不匹配
4. 新 API:
useTemplateRef():获取模板引用onWatcherCleanup():清理副作用- Deferred Teleport:延迟传送
代码示例:
<script setup>
import { useId, useTemplateRef } from 'vue'
// 生成唯一 ID(SSR 安全)
const id = useId()
// 模板引用
const inputRef = useTemplateRef('input')
</script>
<template>
<label :for="id">Name:</label>
<input :id="id" ref="input" />
</template>追问:Lazy Hydration 的使用场景是什么?
23: 什么是 Vapor Mode?(Vue 3.6)
答案要点:
定义:无虚拟 DOM 的编译模式,直接操作真实 DOM
性能:接近 Solid.js,渲染性能大幅提升
特点:
- 同样的源代码,不同的编译输出
- 减少内存占用
- 更快的组件挂载(10 万组件 100ms)
- 可与虚拟 DOM 组件混用
启用方式:组件级别 opt-in,不影响现有代码
原理:
- 编译时分析模板,生成精确的 DOM 操作代码
- 细粒度响应式更新,只更新变化的部分
- 无需创建和 Diff 虚拟 DOM 树
追问:Vapor Mode 会完全替代虚拟 DOM 吗?
24: useId() 解决了什么问题?
答案要点:
- 问题:SSR 时服务端和客户端生成的 ID 不一致,导致水合失败
- 解决:生成全局唯一且 SSR 稳定的 ID
- 使用场景:
- 表单元素的 for/id 关联
- ARIA 属性
- 任何需要唯一 ID 的场景
代码示例:
<script setup>
import { useId } from 'vue'
const id = useId() // 每次调用生成新 ID
const labelId = useId()
</script>
<template>
<div>
<label :for="id">Username:</label>
<input :id="id" :aria-labelledby="labelId" />
</div>
</template>追问:useId() 和 Math.random() 有什么区别?
状态管理
25: Pinia 和 Vuex 有什么区别?为什么推荐 Pinia?
答案要点:
Pinia 优势:
- 更简洁:无 mutations,直接修改 state
- TypeScript 支持更好:完整的类型推导
- 模块化:天然支持,无需嵌套
- 体积更小:约 1KB
- DevTools 支持:时间旅行、热更新
- Vue 3 原生支持:使用 Composition API
Vuex 劣势:
- mutations 样板代码多
- 模块嵌套复杂
- TypeScript 支持弱
代码对比:
// Pinia
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, double, increment }
})
// 使用
const store = useCounterStore()
store.count++ // 直接修改追问:Pinia 如何实现时间旅行?
26: 如何在 Pinia 中组织大型应用的状态?
答案要点:
按功能模块拆分:
- 用户模块:
useUserStore - 购物车模块:
useCartStore - 产品模块:
useProductStore
Store 组合:
- Store 之间可以相互引用
- 使用组合式函数复用逻辑
持久化:
- 使用
pinia-plugin-persistedstate
代码示例:
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const user = ref(null)
const isLoggedIn = computed(() => !!user.value)
async function login(credentials) {
user.value = await api.login(credentials)
}
return { user, isLoggedIn, login }
})
// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
const userStore = useUserStore() // 引用其他 store
const items = ref([])
const total = computed(() => {
if (!userStore.isLoggedIn) return 0
return items.value.reduce((sum, item) => sum + item.price, 0)
})
return { items, total }
})追问:Pinia 的 Store 何时被创建?
性能优化
27: Vue 3 有哪些性能优化手段?
答案要点:
1. 编译时优化:
- 静态提升
- Patch Flag
- Block Tree
- 事件缓存
2. 运行时优化:
- 响应式系统优化(Proxy)
- 虚拟 DOM 优化(双端 Diff)
- 组件懒加载
- KeepAlive 缓存
3. 代码层面:
- 使用
v-once渲染静态内容 - 使用
v-memo缓存子树 - 合理使用
computed(有缓存) - 避免不必要的响应式(
shallowRef、shallowReactive) - 虚拟列表(大数据)
代码示例:
<template>
<!-- v-once: 只渲染一次 -->
<div v-once>{{ staticContent }}</div>
<!-- v-memo: 缓存子树 -->
<div v-memo="[item.id, item.selected]">
{{ item.name }}
</div>
<!-- 虚拟滚动 -->
<VirtualList :items="largeList" :item-height="50" />
</template>
<script setup>
import { shallowRef } from 'vue'
// 浅层响应式(只追踪根级别)
const bigData = shallowRef({ /* 大对象 */ })
</script>追问:v-memo 的使用场景是什么?
28: 如何优化大列表渲染?
答案要点:
1. 虚拟滚动:
- 只渲染可见区域的项
- 使用
vue-virtual-scroller或自己实现
2. 分页加载:
- 懒加载、无限滚动
3. v-memo:
- 缓存列表项,避免不必要的更新
4. shallowRef:
- 大数据使用浅层响应式
5. key 优化:
- 使用稳定的唯一 key
虚拟滚动示例:
<script setup>
import { ref, computed } from 'vue'
const items = ref(Array.from({ length: 10000 }, (_, i) => i))
const scrollTop = ref(0)
const itemHeight = 50
const visibleCount = 20
const visibleItems = computed(() => {
const start = Math.floor(scrollTop.value / itemHeight)
return items.value.slice(start, start + visibleCount)
})
const offsetY = computed(() => {
return Math.floor(scrollTop.value / itemHeight) * itemHeight
})
</script>
<template>
<div class="container" @scroll="scrollTop = $event.target.scrollTop">
<div :style="{ height: items.length * itemHeight + 'px' }">
<div :style="{ transform: `translateY(${offsetY}px)` }">
<div v-for="item in visibleItems" :key="item" :style="{ height: itemHeight + 'px' }">
{{ item }}
</div>
</div>
</div>
</div>
</template>追问:虚拟滚动的原理是什么?
29: 什么是 Tree-shaking?Vue 3 如何支持?
答案要点:
- 定义:移除未使用的代码,减小打包体积
- Vue 3 支持:
- 所有 API 都是具名导出
- 未使用的功能不会打包
- 核心运行时约 13.5KB(gzip)
示例:
// 只导入使用的 API
import { ref, computed } from 'vue'
// reactive、watch 等未使用的不会打包
// ❌ 全量导入(不支持 Tree-shaking)
import Vue from 'vue'效果:
- 只使用基础功能:约 13.5KB
- 使用全部功能:约 22.5KB
- Vue 2 最小:约 23KB
追问:如何查看打包后的体积?
SSR 与同构
30: Vue 3 的 SSR 有哪些改进?
答案要点:
1. 性能提升:
- 编译优化(静态提升在 SSR 中更有效)
- 流式渲染支持
- SSR 性能提升 2~3 倍
2. Lazy Hydration:
- 按需水合组件
- 减少首屏 JavaScript 执行时间
3. useId():
- 解决 SSR 水合不匹配问题
4. data-allow-mismatch:
- 允许特定内容不匹配(如时间戳)
Lazy Hydration 示例:
import { defineAsyncComponent, hydrateOnVisible } from 'vue'
const HeavyComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
hydrate: hydrateOnVisible() // 可见时才水合
})
// 其他策略
// hydrateOnIdle() - 浏览器空闲时
// hydrateOnInteraction() - 交互时
// hydrateOnMediaQuery() - 媒体查询匹配时追问:什么是水合(Hydration)?
工程化实践
31: 如何在 Vue 3 项目中使用 TypeScript?
答案要点:
1. 组件类型定义:
<script setup lang="ts">
import type { Ref } from 'vue'
// Props 类型
interface Props {
msg: string
count?: number
}
const props = withDefaults(defineProps<Props>(), {
count: 0
})
// Emits 类型
const emit = defineEmits<{
update: [value: number]
delete: []
}>()
// Ref 类型
const count: Ref<number> = ref(0)
const user = ref<User | null>(null)
// Computed 类型
const double = computed<number>(() => count.value * 2)
</script>2. 组合式函数类型:
import type { Ref } from 'vue'
export function useMouse(): { x: Ref<number>, y: Ref<number> } {
const x = ref(0)
const y = ref(0)
// ...
return { x, y }
}追问:如何为第三方组件库添加类型?
32: Vue 3 项目的目录结构最佳实践是什么?
答案要点:
src/
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── base/ # 基础组件(BaseButton、BaseInput)
│ └── common/ # 业务组件
├── composables/ # 组合式函数
│ ├── useAuth.ts
│ └── useFetch.ts
├── layouts/ # 布局组件
├── pages/ # 页面组件
│ └── user/
│ ├── index.vue
│ └── [id].vue
├── router/ # 路由配置
├── stores/ # Pinia stores
│ ├── user.ts
│ └── cart.ts
├── types/ # TypeScript 类型
├── utils/ # 工具函数
├── App.vue
└── main.ts命名规范:
- 组件:PascalCase(
UserProfile.vue) - 组合式函数:use 开头(
useAuth.ts) - Store:use 开头 + Store 结尾(
useUserStore)
追问:如何组织大型应用的路由?
33: Vue 3 的测试策略是什么?
答案要点:
1. 单元测试:
- 工具:Vitest(推荐)、Jest
- 测试组合式函数、工具函数
2. 组件测试:
- 工具:
@vue/test-utils - 测试组件行为、props、emits
3. E2E 测试:
- 工具:Playwright、Cypress
- 测试完整用户流程
组件测试示例:
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'
describe('Counter', () => {
it('increments count when button is clicked', async () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('0')
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('1')
})
it('emits update event', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('update')).toBeTruthy()
expect(wrapper.emitted('update')?.[0]).toEqual([1])
})
})追问:如何测试异步组件?
面试高频题
34: Vue 2 和 Vue 3 的响应式原理有什么区别?
核心区别:
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 实现方式 | Object.defineProperty | Proxy |
| 数组索引 | ❌ 不支持 | ✅ 支持 |
| 新增属性 | ❌ 需要 $set | ✅ 支持 |
| 删除属性 | ❌ 需要 $delete | ✅ 支持 |
| Map/Set | ❌ 不支持 | ✅ 支持 |
| 性能 | 初始化时递归遍历 | 懒代理,按需递归 |
| 兼容性 | IE9+ | 不支持 IE11 |
追问:为什么 Vue 3 不再支持 IE11?
35: 为什么 Vue 3 性能比 Vue 2 好?
答案要点:
1. 编译时优化:
- 静态提升(减少创建开销)
- Patch Flag(精确更新)
- Block Tree(扁平化 Diff)
- 事件缓存(减少更新)
2. 运行时优化:
- Proxy 响应式(性能更好)
- 更快的组件初始化
- 更高效的 Diff 算法
3. 体积优化:
- Tree-shaking(按需打包)
- 核心库减小 41%
性能数据:
- 更新性能提升 1.3~2 倍
- SSR 性能提升 2~3 倍
- 内存使用减少 54%
追问:Vue 3 在什么场景下性能提升最明显?
36: 什么时候使用 Composition API,什么时候使用 Options API?
答案要点:
使用 Composition API:
- 逻辑复用需求多
- 大型复杂组件
- 需要更好的 TypeScript 支持
- 团队熟悉函数式编程
使用 Options API:
- 简单组件
- 团队更熟悉 Options API
- 维护老项目
建议:
- 新项目优先 Composition API
- 两种 API 可以混用
- 根据团队情况选择
追问:Composition API 会完全替代 Options API 吗?
37: Vue 3 的 Teleport 和 React Portal 有什么区别?
答案要点:
相同点:
- 都可以将内容渲染到 DOM 其他位置
- 保持组件逻辑关系
不同点:
| 特性 | Vue Teleport | React Portal |
|---|---|---|
| 语法 | 组件形式 | API 调用 |
| 禁用 | 支持 disabled | 不支持 |
| 延迟挂载 | 支持 defer | 不支持 |
| 多目标 | 单个 to | 单个 container |
代码示例:
<!-- Vue Teleport -->
<Teleport to="body" :disabled="isMobile" defer>
<Modal />
</Teleport>
// React Portal
ReactDOM.createPortal(
<Modal />,
document.body
)追问:Teleport 的使用场景有哪些?
38: 如何理解 Vue 3 的"编译时 + 运行时"设计?
答案要点:
编译时:
- 模板编译为渲染函数
- 静态分析优化(静态提升、Patch Flag)
- 生成优化的代码
运行时:
- 执行渲染函数
- 响应式系统
- 虚拟 DOM Diff
- 组件生命周期
优势:
- 编译时优化性能
- 运行时保持灵活性
- 平衡开发体验和性能
对比:
- Svelte:纯编译时(无运行时)
- React:偏运行时(编译优化少)
- Vue:编译时 + 运行时平衡
追问:Vapor Mode 是纯编译时吗?
39: Vue 3 如何实现代码分割和懒加载?
答案要点:
1. 路由懒加载:
const routes = [
{
path: '/user',
component: () => import('./pages/User.vue')
}
]2. 组件懒加载:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/Heavy.vue')
)
// 带选项
const AsyncComp = defineAsyncComponent({
loader: () => import('./Heavy.vue'),
loadingComponent: Loading,
errorComponent: Error,
delay: 200,
timeout: 3000
})3. Vite 动态导入:
// 预加载
const modules = import.meta.glob('./components/*.vue', { eager: true })
// 懒加载
const modules = import.meta.glob('./components/*.vue')追问:如何预加载关键路由?
40: 如何在 Vue 3 中实现权限控制?
答案要点:
1. 路由级别:
// router/index.ts
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
next('/login')
} else if (to.meta.roles && !to.meta.roles.includes(userStore.role)) {
next('/403')
} else {
next()
}
})2. 组件级别:
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const canEdit = computed(() => userStore.hasPermission('edit'))
</script>
<template>
<button v-if="canEdit">编辑</button>
</template>3. 指令级别:
// directives/permission.ts
export const vPermission = {
mounted(el, binding) {
const userStore = useUserStore()
if (!userStore.hasPermission(binding.value)) {
el.remove()
}
}
}
// 使用
<button v-permission="'delete'">删除</button>追问:如何实现动态路由?