Vue3 面试题汇总

系统梳理 Vue3 高频面试考点,从响应式原理到工程实践,助你从容应对技术面试。

一、前言

Vue3 自发布以来已成为前端面试的核心考点。相比 Vue2,Vue3 在响应式系统、组合式 API、编译优化等方面都有重大变化。本章将高频面试题按模块整理,采用问答形式,每个问题都附带详细解答和代码示例,帮助你深入理解 Vue3 的核心原理。

二、响应式原理

2.1 Vue3 为什么使用 Proxy 替代 Object.defineProperty?

答: Vue3 使用 Proxy 替代 Object.defineProperty 主要基于以下原因:

对比维度 Object.defineProperty (Vue2) Proxy (Vue3)
拦截能力 只能拦截已有属性的读写 可拦截 13 种操作(get/set/has/deleteProperty 等)
新增属性 需用 Vue.set / this.$set 自动响应新增属性
数组索引 无法直接监听索引和 length 天然支持数组索引和长度变化
删除属性 无法监听属性删除 通过 deleteProperty 拦截
Map/Set 不支持 支持原生集合类型
嵌套对象 递归遍历所有属性进行转换 懒递归,访问时才代理
性能 初始化时递归遍历,大数据对象有性能损耗 懒代理,初始化更快
// Vue2 的局限:新增属性不响应
// this.list[0] = 'new' // 不触发更新
// Vue.set(this.list, 0, 'new') // 需要显式调用

// Vue3 的 Proxy 天然支持
const state = reactive({ list: ['a', 'b'] })
state.list[0] = 'new'      // 触发更新
state.list.push('c')       // 触发更新
state.newProp = 'value'    // 触发更新
delete state.newProp       // 触发更新

2.2 Reflect 在 Vue3 响应式中的作用是什么?

答: Reflect 是 ES6 提供的操作对象 API,在 Vue3 响应式中有两个核心作用:

  1. 保证 this 指向正确:Reflect.get/set 的 receiver 参数确保访问器属性中的 this 指向代理对象而非原始对象
  2. 规范化返回值:Reflect 操作都有明确的返回值,便于错误处理
const raw = {
  _value: 0,
  get value() {
    // 这里的 this 应该是代理对象,以便触发依赖收集
    return this._value
  }
}

const proxy = new Proxy(raw, {
  get(target, key, receiver) {
    // 使用 Reflect.get 传递 receiver,确保 this 指向 proxy
    const result = Reflect.get(target, key, receiver)
    track(target, key)  // 依赖收集
    return result
  },
  set(target, key, value, receiver) {
    const oldValue = target[key]
    // Reflect.set 返回布尔值表示是否设置成功
    const result = Reflect.set(target, key, value, receiver)
    if (oldValue !== value) {
      trigger(target, key)  // 触发更新
    }
    return result
  }
})

2.3 手写简易 reactive 实现

// 简易 reactive 实现
const targetMap = new WeakMap()
let activeEffect = null

function effect(fn) {
  activeEffect = fn
  fn()           // 立即执行,触发依赖收集
  activeEffect = null
}

function track(target, key) {
  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, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(fn => fn())
  }
}

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver)
      track(target, key)
      // 懒递归:访问嵌套对象时才转为响应式
      if (result && typeof result === 'object') {
        return reactive(result)
      }
      return result
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      if (oldValue !== value) {
        trigger(target, key)
      }
      return result
    }
  })
}

// 使用示例
const state = reactive({ count: 0, nested: { value: 1 } })

effect(() => {
  console.log('count:', state.count)
})

state.count++        // 输出: count: 1
state.nested.value++ // 嵌套对象也是响应式的

三、Composition API

3.1 Composition API 的设计意图与优势

答: Composition API 的设计意图是解决 Options API 在大型组件中代码组织困难的问题。

Options API 的痛点:

// Options API:同一功能的代码分散在不同选项中
export default {
  data() {
    return {
      searchQuery: '',
      searchResults: [],
      loading: false
    }
  },
  computed: {
    filteredResults() { /* ... */ }  // 搜索功能相关
  },
  watch: {
    searchQuery() { /* ... */ }      // 搜索功能相关
  },
  methods: {
    search() { /* ... */ },           // 搜索功能相关
    handlePagination() { /* ... */ }  // 分页功能相关
  },
  mounted() {
    this.search()                     // 搜索功能相关
  }
  // 搜索功能的逻辑分散在 data/computed/watch/methods/mounted 中
}

Composition API 的优势:

<script setup>
import { ref, computed, watch, onMounted } from 'vue'

// 搜索功能逻辑聚合在一起
function useSearch() {
  const searchQuery = ref('')
  const searchResults = ref([])
  const loading = ref(false)

  const filteredResults = computed(() => {
    return searchResults.value.filter(/* ... */)
  })

  watch(searchQuery, () => {
    // 防抖搜索
  })

  async function search() {
    loading.value = true
    searchResults.value = await fetchResults(searchQuery.value)
    loading.value = false
  }

  onMounted(search)

  return { searchQuery, filteredResults, loading, search }
}

// 分页功能逻辑聚合在一起
function usePagination() {
  const currentPage = ref(1)
  const pageSize = ref(10)
  const total = ref(0)

  function changePage(page) {
    currentPage.value = page
  }

  return { currentPage, pageSize, total, changePage }
}

// 在组件中使用
const { searchQuery, filteredResults, loading } = useSearch()
const { currentPage, changePage } = usePagination()
</script>

优势总结:

  • 逻辑复用:通过函数提取可复用逻辑(Composables)
  • 代码组织:同一功能的代码聚合在一起,便于维护
  • TypeScript 友好:更好的类型推断支持
  • Tree Shaking:按需导入,未使用的 API 不会被打包

3.2 ref 与 reactive 的区别

对比项 ref reactive
数据类型 任意类型(原始值和对象) 仅对象(对象、数组、Map、Set)
访问方式 .value 直接访问属性
深层响应式 默认深层 默认深层
替换整个对象 可以(ref.value = newObj 会失去响应式
解构/展开 解构后失去响应式 解构后失去响应式
适用场景 原始值、需要替换的对象 对象、数组、复杂数据结构
<script setup>
import { ref, reactive } from 'vue'

// ref:适合原始值和需要替换的对象
const count = ref(0)
console.log(count.value)  // 0

count.value++             // 正确

const user = ref({ name: '张三' })
user.value = { name: '李四' }  // 可以替换整个对象

// reactive:适合对象和数组
const state = reactive({
  user: { name: '张三', age: 25 },
  hobbies: ['读书', '游泳']
})

state.user.name = '李四'   // 正确
state.hobbies.push('跑步') // 正确

// 注意:不能替换整个 reactive 对象
// state = { user: {} }    // 错误!会失去响应式

// 解构问题
const { name } = state.user  // name 不是响应式的
// 解决:使用 toRefs
import { toRefs } from 'vue'
const { user } = toRefs(state)  // user 是 ref,保持响应式
</script>

3.3 computed 与 watch 的区别

对比项 computed watch
用途 基于依赖计算派生值 监听数据变化执行副作用
返回值 有(返回计算结果)
缓存 有(依赖不变不重新计算)
使用场景 派生状态、模板中复杂计算 数据变化时执行异步操作、DOM 操作
立即执行 否(首次访问才计算) 可配置 immediate: true
深度监听 自动追踪访问的属性 需配置 deep: true
<script setup>
import { ref, computed, watch } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// computed:有缓存,依赖不变直接返回缓存值
const fullName = computed(() => {
  console.log('computed 执行')
  return firstName.value + lastName.value
})

// watch:执行副作用
watch(fullName, (newVal, oldVal) => {
  console.log('姓名变化:', oldVal, '->', newVal)
})

// watch 监听多个源
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log('名字或姓氏变化')
})

// watchEffect:立即执行,自动追踪依赖
import { watchEffect } from 'vue'
watchEffect(() => {
  // 自动追踪 firstName 和 lastName
  console.log('当前全名:', firstName.value + lastName.value)
})
</script>

四、生命周期

4.1 Vue3 生命周期变化

创建阶段

beforeCreate

setup

created

beforeMount

onBeforeMount

mounted

onMounted

更新阶段

beforeUpdate

onBeforeUpdate

updated

onUpdated

卸载阶段

beforeUnmount

onBeforeUnmount

unmounted

onUnmounted

Options API Composition API 说明
beforeCreate 无(用 setup 替代) 实例初始化后,数据观测前
created 无(用 setup 替代) 实例创建完成
beforeMount onBeforeMount 挂载开始前
mounted onMounted 挂载完成,可访问 DOM
beforeUpdate onBeforeUpdate 数据更新,DOM 更新前
updated onUpdated DOM 更新完成
beforeUnmount onBeforeUnmount 卸载开始前
unmounted onUnmounted 卸载完成
activated onActivated keep-alive 组件激活
deactivated onDeactivated keep-alive 组件失活
errorCaptured onErrorCaptured 捕获后代组件错误
renderTracked onRenderTracked 调试:依赖被追踪时
renderTriggered onRenderTriggered 调试:依赖触发更新时
<script setup>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from 'vue'

// setup 相当于 beforeCreate + created
console.log('setup 执行')

onBeforeMount(() => console.log('挂载前'))
onMounted(() => console.log('挂载完成,DOM 可操作'))
onBeforeUpdate(() => console.log('更新前'))
onUpdated(() => console.log('更新完成'))
onBeforeUnmount(() => console.log('卸载前,清理副作用'))
onUnmounted(() => console.log('卸载完成'))
</script>

4.2 setup 执行时机与注意点

执行时机: setup 在组件实例创建之后、生命周期钩子注册之前执行,相当于 Vue2 的 beforeCreatecreated 的结合。

注意点:

<script setup>
import { ref, onMounted } from 'vue'

// 1. setup 中 this 是 undefined
console.log(this) // undefined

// 2. 同步执行,不要在 setup 顶部写耗时操作
const data = ref(null)

// 3. 异步操作放在生命周期或单独函数中
async function fetchData() {
  const res = await fetch('/api/data')
  data.value = await res.json()
}

onMounted(fetchData)

// 4. 顶层 await 在 script setup 中会被编译为 async setup()
// const res = await fetch('/api/data') // 组件会变成异步依赖
</script>

五、新特性

5.1 Teleport 的使用场景

Teleport 用于将组件的模板渲染到 DOM 中的其他位置。

常见使用场景:

  • 模态框/对话框(避免 z-index 和父级样式干扰)
  • 通知/Toast 提示
  • 全局加载遮罩
<!-- 子组件中定义,但渲染到 body 下 -->
<template>
  <div class="content">
    <button @click="show = true">打开模态框</button>

    <Teleport to="body">
      <div v-if="show" class="modal-overlay" @click="show = false">
        <div class="modal" @click.stop>
          <h2>模态框标题</h2>
          <p>内容...</p>
          <button @click="show = false">关闭</button>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const show = ref(false)
</script>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
}
.modal {
  background: white;
  padding: 20px;
  border-radius: 8px;
}
</style>

5.2 Suspense 的使用场景

Suspense 用于在异步组件加载时显示 fallback 内容。

<template>
  <Suspense>
    <!-- 默认内容:异步组件加载完成后显示 -->
    <template #default>
      <AsyncDashboard />
    </template>

    <!-- 加载中状态 -->
    <template #fallback>
      <div class="loading">
        <span>加载中...</span>
      </div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'

const AsyncDashboard = defineAsyncComponent(() =>
  import('./components/Dashboard.vue')
)
</script>

六、状态管理

6.1 Pinia 与 Vuex 对比

特性 Vuex 4 Pinia
Vue3 支持 支持 原生支持,官方推荐
API 风格 Options(mutations/actions) 类似组件的 setup 风格
TypeScript 需要复杂的类型封装 完整的类型推断
代码分割 困难 天然支持模块热更新
体积 约 1KB(gzip) 约 1KB(gzip),更轻量
模块 命名空间复杂 无命名空间概念,直接导入
Devtools 支持 支持,体验更好
SSR 支持 支持
// Pinia Store 示例
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// 定义 Store
export const useUserStore = defineStore('user', () => {
  // State
  const token = ref(localStorage.getItem('token') || '')
  const userInfo = ref(null)

  // Getters
  const isLoggedIn = computed(() => !!token.value)

  // Actions
  async function login(credentials) {
    const res = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials)
    })
    const data = await res.json()
    token.value = data.token
    userInfo.value = data.user
    localStorage.setItem('token', data.token)
  }

  function logout() {
    token.value = ''
    userInfo.value = null
    localStorage.removeItem('token')
  }

  return { token, userInfo, isLoggedIn, login, logout }
})

// 组件中使用
// import { useUserStore } from '@/stores/user'
// const userStore = useUserStore()
// userStore.login({ username, password })

七、性能优化

7.1 Vue3 性能优化手段

编译时优化:

  • 静态提升(Static Hoisting)
  • PatchFlag 标记动态节点
  • Block Tree 减少 Diff 范围
  • 树摇优化(Tree Shaking)

运行时优化:

  • v-once:只渲染一次
  • v-memo:条件缓存子树
  • shallowRef / shallowReactive:浅层响应式
  • 异步组件懒加载

代码示例:

<template>
  <div>
    <!-- 静态内容只创建一次 -->
    <header v-once>
      <h1>网站标题</h1>
      <nav>导航...</nav>
    </header>

    <!-- v-memo 条件缓存 -->
    <div v-for="item in list" :key="item.id" v-memo="[item.selected]">
      <h3>{{ item.title }}</h3>
      <p>{{ item.content }}</p>
      <span v-if="item.selected">已选中</span>
    </div>
  </div>
</template>

<script setup>
import { shallowRef } from 'vue'

// 大数据对象使用浅层响应式
const hugeData = shallowRef({
  list: Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `item-${i}` }))
})
</script>

7.2 Vue3 编译优化原理

模板编译

解析为 AST

静态分析

生成渲染函数

静态节点提升

标记 PatchFlag

构建 Block Tree

运行时渲染

跳过静态节点

只对比 PatchFlag 标记部分

Block 内只遍历动态节点

八、手写题

8.1 实现简易 useCount Composable

// composables/useCount.js
import { ref, computed } from 'vue'

export function useCount(initial = 0, { min = -Infinity, max = Infinity } = {}) {
  const count = ref(initial)

  // 确保初始值在范围内
  if (count.value < min) count.value = min
  if (count.value > max) count.value = max

  const inc = (n = 1) => {
    count.value = Math.min(count.value + n, max)
  }

  const dec = (n = 1) => {
    count.value = Math.max(count.value - n, min)
  }

  const set = (n) => {
    count.value = Math.max(min, Math.min(n, max))
  }

  const reset = () => {
    count.value = initial
  }

  const isMin = computed(() => count.value <= min)
  const isMax = computed(() => count.value >= max)

  return {
    count: readonly(count),  // 外部只读
    inc,
    dec,
    set,
    reset,
    isMin,
    isMax
  }
}

// 使用示例
// import { useCount } from './composables/useCount'
// const { count, inc, dec, isMin, isMax } = useCount(0, { min: 0, max: 10 })

8.2 实现简易事件总线

// utils/eventBus.js
class EventBus {
  constructor() {
    this.events = new Map()
  }

  on(event, callback) {
    if (!this.events.has(event)) {
      this.events.set(event, new Set())
    }
    this.events.get(event).add(callback)

    // 返回取消订阅函数
    return () => this.off(event, callback)
  }

  off(event, callback) {
    const callbacks = this.events.get(event)
    if (callbacks) {
      callbacks.delete(callback)
      if (callbacks.size === 0) {
        this.events.delete(event)
      }
    }
  }

  emit(event, ...args) {
    const callbacks = this.events.get(event)
    if (callbacks) {
      callbacks.forEach(callback => callback(...args))
    }
  }

  once(event, callback) {
    const wrapper = (...args) => {
      callback(...args)
      this.off(event, wrapper)
    }
    this.on(event, wrapper)
  }

  clear() {
    this.events.clear()
  }
}

export const eventBus = new EventBus()

// 使用示例
// import { eventBus } from './utils/eventBus'
// const unsubscribe = eventBus.on('user-login', (user) => console.log(user))
// eventBus.emit('user-login', { name: '张三' })
// unsubscribe() // 取消订阅

九、高频面试题速查

Q1:Vue3 的响应式原理是什么?

Vue3 使用 Proxy 代理对象,配合 Reflect 操作目标对象。通过 track 在 getter 中收集依赖,trigger 在 setter 中触发更新。采用懒递归策略,访问嵌套对象时才创建代理。

Q2:Composition API 和 Options API 怎么选?

  • 小型组件、逻辑简单:Options API
  • 大型组件、逻辑复杂、需要复用逻辑:Composition API
  • 新项目:推荐 Composition API + <script setup>
  • TypeScript 项目:推荐 Composition API

Q3:watch 和 watchEffect 的区别?

  • watch:显式指定监听源,懒执行(默认不立即执行),可获取新旧值
  • watchEffect:自动追踪依赖,立即执行,无法获取旧值

Q4:Vue3 中组件通信方式有哪些?

方式 适用场景
Props / Emits 父子组件
provide / inject 跨层级组件
ref(模板引用) 父组件调用子组件方法
Pinia / Vuex 全局状态
eventBus / mitt 任意组件(不推荐大规模使用)
attrs / slots 透传属性和插槽

Q5:nextTick 的作用?

Vue 的 DOM 更新是异步的,nextTick 用于在下次 DOM 更新循环结束后执行回调,确保获取到更新后的 DOM。

import { nextTick, ref } from 'vue'

const count = ref(0)
const el = ref(null)

async function increment() {
  count.value++
  // DOM 还未更新
  console.log(el.value.textContent) // 0

  await nextTick()
  // DOM 已更新
  console.log(el.value.textContent) // 1
}

Q6:Vue3 的自定义指令生命周期变化?

Vue2 Vue3
bind mounted
inserted 移除(用 mounted 替代)
update updated
componentUpdated 移除
unbind unmounted
const vFocus = {
  mounted(el) {
    el.focus()
  }
}

// 使用
// <input v-focus />

十、总结

本章系统整理了 Vue3 高频面试考点:

  1. 响应式原理:Proxy 替代 Object.defineProperty,Reflect 保证 this 指向
  2. Composition API:逻辑聚合、代码复用、TypeScript 友好
  3. ref vs reactive:原始值用 ref,对象用 reactive,注意解构问题
  4. 生命周期:setup 替代 beforeCreate/created,命名加 on 前缀
  5. 新特性:Teleport 解决层级问题,Suspense 处理异步加载
  6. 状态管理:Pinia 是 Vuex 的继任者,API 更简洁
  7. 性能优化:编译时 PatchFlag + Block Tree,运行时 v-memo + 懒加载
  8. 手写题:掌握简易 reactive、useCount、事件总线实现

十一、练习题

  1. 手写实现一个支持数组和嵌套对象的 reactive 函数。
  2. 使用 Composition API 重构一个使用 Options API 编写的复杂组件。
  3. 实现一个 useLocalStorage Composable,支持响应式读写 localStorage。
  4. 对比测试 refreactive 在存储大型对象时的内存占用差异。
  5. 整理 10 道 Vue3 面试题并写出详细解答,模拟面试场景。
Logo

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

更多推荐