16-Vue3 面试题汇总
·
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 响应式中有两个核心作用:
- 保证 this 指向正确:Reflect.get/set 的 receiver 参数确保访问器属性中的
this指向代理对象而非原始对象 - 规范化返回值: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 生命周期变化
| 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 的 beforeCreate 和 created 的结合。
注意点:
<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 编译优化原理
八、手写题
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 高频面试考点:
- 响应式原理:Proxy 替代 Object.defineProperty,Reflect 保证 this 指向
- Composition API:逻辑聚合、代码复用、TypeScript 友好
- ref vs reactive:原始值用 ref,对象用 reactive,注意解构问题
- 生命周期:setup 替代 beforeCreate/created,命名加 on 前缀
- 新特性:Teleport 解决层级问题,Suspense 处理异步加载
- 状态管理:Pinia 是 Vuex 的继任者,API 更简洁
- 性能优化:编译时 PatchFlag + Block Tree,运行时 v-memo + 懒加载
- 手写题:掌握简易 reactive、useCount、事件总线实现
十一、练习题
- 手写实现一个支持数组和嵌套对象的
reactive函数。 - 使用 Composition API 重构一个使用 Options API 编写的复杂组件。
- 实现一个
useLocalStorageComposable,支持响应式读写 localStorage。 - 对比测试
ref和reactive在存储大型对象时的内存占用差异。 - 整理 10 道 Vue3 面试题并写出详细解答,模拟面试场景。
更多推荐

所有评论(0)