Skip to content

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 支持

代码示例:

vue
<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>>> 存储依赖关系

简化实现:

typescript
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,便于解构

代码示例:

typescript
// 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+)

代码示例:

typescript
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 操作、本地存储

代码示例:

typescript
// 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 倍

代码示例:

typescript
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 类型推导更强

代码对比:

vue
<!-- 普通 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 开头(如 useMouseuseFetch
  • 返回值:返回 ref 对象,便于解构
  • 参数规范化:使用 toValue()MaybeRef 类型
  • 副作用清理:使用 onUnmountedonWatcherCleanup

代码示例:

typescript
// 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 解构:

vue
<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

代码示例:

vue
<!-- 父组件 -->
<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
    • 支持修饰符

代码示例:

vue
<!-- 子组件 -->
<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() 本身相当于 beforeCreatecreated
  • 钩子可以多次调用
  • 钩子在同步执行期间调用才有效

代码示例:

typescript
import { onMounted, onUnmounted } from 'vue'

// 可以多次调用
onMounted(() => {
  console.log('mounted 1')
})

onMounted(() => {
  console.log('mounted 2')
})

// 清理副作用
onUnmounted(() => {
  // 清理定时器、事件监听等
})

追问:为什么 Vue 3 移除了 beforeCreate 和 created?


16: 父子组件的生命周期执行顺序是什么?

答案要点

挂载阶段

  1. 父 beforeMount
  2. 子 beforeMount
  3. 子 mounted
  4. 父 mounted

更新阶段

  1. 父 beforeUpdate
  2. 子 beforeUpdate
  3. 子 updated
  4. 父 updated

卸载阶段

  1. 父 beforeUnmount
  2. 子 beforeUnmount
  3. 子 unmounted
  4. 父 unmounted

原则:子组件先完成,父组件后完成

追问:为什么是这个顺序?


虚拟 DOM 与 Diff 算法

17: 什么是虚拟 DOM?为什么需要它?

答案要点

  • 定义:用 JavaScript 对象描述 DOM 结构

  • 优势

    • 跨平台:可渲染到不同平台(Web、Native、Canvas)
    • 性能优化:批量更新、减少 DOM 操作
    • 开发体验:声明式编程,无需手动操作 DOM
  • 劣势

    • 内存开销(维护虚拟 DOM 树)
    • 首次渲染慢于直接操作 DOM

VNode 结构:

typescript
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 移动

比较策略

  1. 旧头 vs 新头
  2. 旧尾 vs 新尾
  3. 旧头 vs 新尾
  4. 旧尾 vs 新头
  5. 通过 key 查找

代码示例:

vue
<!-- ❌ 不使用 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. 缓存事件处理器

  • 自动缓存内联事件处理器

代码示例:

vue
<template>
  <div>
    <p>Static text</p> <!-- 静态提升 -->
    <p>{{ dynamic }}</p> <!-- Patch Flag: TEXT -->
    <p :class="cls">Text</p> <!-- Patch Flag: CLASS -->
  </div>
</template>

编译后:

javascript
// 静态提升
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):动态 class
    • 4 (STYLE):动态 style
    • 8 (PROPS):动态属性
    • 16 (FULL_PROPS):有动态 key 的属性
    • 32 (HYDRATE_EVENTS):事件监听器
    • 64 (STABLE_FRAGMENT):稳定的 Fragment
    • 128 (KEYED_FRAGMENT):有 key 的 Fragment
    • 256 (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():生成稳定的唯一 ID
  • data-allow-mismatch:允许特定不匹配

4. 新 API

  • useTemplateRef():获取模板引用
  • onWatcherCleanup():清理副作用
  • Deferred Teleport:延迟传送

代码示例:

vue
<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 的场景

代码示例:

vue
<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 支持弱

代码对比:

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

代码示例:

typescript
// 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(有缓存)
  • 避免不必要的响应式(shallowRefshallowReactive
  • 虚拟列表(大数据)

代码示例:

vue
<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

虚拟滚动示例:

vue
<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)

示例

typescript
// 只导入使用的 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 示例:

typescript
import { defineAsyncComponent, hydrateOnVisible } from 'vue'

const HeavyComponent = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  hydrate: hydrateOnVisible() // 可见时才水合
})

// 其他策略
// hydrateOnIdle() - 浏览器空闲时
// hydrateOnInteraction() - 交互时
// hydrateOnMediaQuery() - 媒体查询匹配时

追问:什么是水合(Hydration)?


工程化实践

31: 如何在 Vue 3 项目中使用 TypeScript?

答案要点

1. 组件类型定义

vue
<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. 组合式函数类型

typescript
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
  • 测试完整用户流程

组件测试示例:

typescript
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 2Vue 3
实现方式Object.definePropertyProxy
数组索引❌ 不支持✅ 支持
新增属性❌ 需要 $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 TeleportReact Portal
语法组件形式API 调用
禁用支持 disabled不支持
延迟挂载支持 defer不支持
多目标单个 to单个 container

代码示例:

vue
<!-- 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. 路由懒加载

typescript
const routes = [
  {
    path: '/user',
    component: () => import('./pages/User.vue')
  }
]

2. 组件懒加载

typescript
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 动态导入

typescript
// 预加载
const modules = import.meta.glob('./components/*.vue', { eager: true })

// 懒加载
const modules = import.meta.glob('./components/*.vue')

追问:如何预加载关键路由?


40: 如何在 Vue 3 中实现权限控制?

答案要点

1. 路由级别

typescript
// 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. 组件级别

vue
<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. 指令级别

typescript
// directives/permission.ts
export const vPermission = {
  mounted(el, binding) {
    const userStore = useUserStore()
    if (!userStore.hasPermission(binding.value)) {
      el.remove()
    }
  }
}

// 使用
<button v-permission="'delete'">删除</button>

追问:如何实现动态路由?