Vue 3 面试题库
精选高频面试题及详细解答
📅 更新时间: 2025-02
📚 内容来源: Vue 3 官方文档、Pinia 官方文档、Vue Router 4 官方文档
📋 目录
基础题(必会)
1. Composition API 与 Options API 的差异?
难度: ⭐⭐☆☆☆
问题: 请说明 Composition API 和 Options API 的主要区别,以及为什么现在推荐使用 Composition API?
答案:
主要区别:
- 代码组织: Composition API 按逻辑功能分组,Options API 按选项类型分组
- 逻辑复用: Composition API 使用 Composables,Options API 使用 Mixins
- TypeScript 支持: Composition API 类型推断更好
- this 使用: Composition API 不使用 this,Options API 频繁使用 this
- 学习曲线: Options API 更简单,Composition API 需要理解响应式原理
代码对比:
<!-- Composition API(推荐) -->
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
// 响应式状态
const count = ref(0)
const doubled = computed(() => count.value * 2)
// 方法
function increment() {
count.value++
}
// 侦听器
watch(count, (newVal) => {
console.log(`Count changed to ${newVal}`)
})
// 生命周期
onMounted(() => {
console.log('Component mounted')
})
</script>
<template>
<button @click="increment">
Count: {{ count }}, Doubled: {{ doubled }}
</button>
</template><!-- Options API(遗留代码) -->
<script>
export default {
data() {
return {
count: 0
}
},
computed: {
doubled() {
return this.count * 2
}
},
watch: {
count(newVal) {
console.log(`Count changed to ${newVal}`)
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('Component mounted')
}
}
</script>
<template>
<button @click="increment">
Count: {{ count }}, Doubled: {{ doubled }}
</button>
</template>逻辑复用对比:
// Composition API - Composable(推荐)
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// 使用
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()// Options API - Mixin(不推荐)
// mixins/mouse.js
export default {
data() {
return {
x: 0,
y: 0
}
},
mounted() {
window.addEventListener('mousemove', this.update)
},
beforeUnmount() {
window.removeEventListener('mousemove', this.update)
},
methods: {
update(event) {
this.x = event.pageX
this.y = event.pageY
}
}
}
// 使用(可能有命名冲突)
import mouseMixin from '@/mixins/mouse'
export default {
mixins: [mouseMixin]
}追问点:
Q1: Options API 会被废弃吗?
A: 不会被废弃。Vue 3 完全支持 Options API,官方承诺长期维护。两种 API 可以根据项目需求和团队偏好选择,小项目或初学者可以继续使用 Options API。
Q2: Composition API 的性能优势在哪里?
A: 主要体现在三个方面:
- Tree-shaking 友好:未使用的 API 可以被打包工具移除
- 更好的压缩:函数名可以被压缩,而 Options API 的选项名不能压缩
- 减少实例创建开销:setup() 只执行一次,而 Options API 每次都要创建实例
// Composition API - 支持 Tree-shaking
import { ref, computed } from 'vue' // 只导入需要的 API
// Options API - 无法 Tree-shake
export default {
data() { /* 所有选项都会被包含 */ },
computed: { /* ... */ },
methods: { /* ... */ }
}Q3: 如何在 Composition API 中处理 this 的问题?
A: Composition API 不使用 this,所有状态和方法都通过变量和函数定义:
- 状态管理:使用 ref/reactive 替代 data
- 方法定义:使用普通函数替代 methods
- 计算属性:使用 computed() 函数
- 避免 this 绑定问题:箭头函数和普通函数都可以正常使用
// 不需要担心 this 绑定
const handleClick = () => {
count.value++ // 直接访问响应式变量
}
const handleAsync = async () => {
const result = await api.getData()
data.value = result // this 绑定不会丢失
}2. ref 和 reactive 的区别?
难度: ⭐⭐⭐☆☆
问题: ref 和 reactive 有什么区别?什么时候使用 ref,什么时候使用 reactive?
答案:
核心区别:
| 特性 | ref | reactive |
|---|---|---|
| 支持类型 | 任意类型(基本类型、对象) | 仅对象/数组 |
| 访问方式 | 需要 .value | 直接访问 |
| 模板中 | 自动解包,无需 .value | 直接使用 |
| 解构 | 保持响应式 | 会丢失响应式(需 toRefs) |
| 重新赋值 | 可以整体替换 | 不能整体替换 |
| 实现原理 | 包装对象 + Proxy | Proxy |
代码示例:
import { ref, reactive, isRef, isReactive, toRefs } from 'vue'
// ref - 用于任意类型
const count = ref(0)
const user = ref({ name: 'John', age: 25 })
console.log(count.value) // 0
console.log(user.value.name) // 'John'
// 可以整体替换
count.value = 10
user.value = { name: 'Jane', age: 30 }
// reactive - 只能用于对象
const state = reactive({
count: 0,
user: { name: 'John', age: 25 }
})
console.log(state.count) // 0
console.log(state.user.name) // 'John'
// 不能整体替换(会丢失响应式)
// state = { count: 10 } // ❌ 错误
// 应该修改属性
state.count = 10 // ✅ 正确
state.user.name = 'Jane' // ✅ 正确解构问题:
// reactive 解构会丢失响应式
const state = reactive({ count: 0, name: 'Vue' })
let { count, name } = state // ❌ 丢失响应式
// 解决方案 1:使用 toRefs
import { toRefs } from 'vue'
const { count, name } = toRefs(state) // ✅ count 和 name 是 ref
// 解决方案 2:使用 toRef
import { toRef } from 'vue'
const count = toRef(state, 'count') // ✅ count 是 ref
// ref 解构不会丢失响应式
const count = ref(0)
const { value } = count // value 只是数字,但 count 仍是响应式使用建议:
// ✅ 推荐:基本类型用 ref
const count = ref(0)
const message = ref('Hello')
const isLoading = ref(false)
// ✅ 推荐:对象用 reactive(不需要解构时)
const form = reactive({
username: '',
password: '',
remember: false
})
// ✅ 推荐:需要整体替换时用 ref
const user = ref(null)
user.value = await fetchUser() // 可以整体替换
// ❌ 不推荐:对象用 ref(需要频繁 .value)
const form = ref({
username: '',
password: ''
})
form.value.username = 'admin' // 繁琐追问点:
Q1: shallowRef 和 shallowReactive 的区别?
A: shallow 版本只有根级别是响应式的,嵌套对象不是响应式:
- shallowRef:只有 .value 的赋值是响应式的,.value 内部的属性变化不会触发更新
- shallowReactive:只有根级别属性是响应式的,嵌套对象的属性变化不会触发更新
- 使用场景:大型数据结构的性能优化,只关心根级别变化时
import { shallowRef, shallowReactive } from 'vue'
// shallowRef 示例
const user = shallowRef({ name: 'John', profile: { age: 25 } })
user.value.name = 'Jane' // ❌ 不会触发更新
user.value.profile.age = 30 // ❌ 不会触发更新
user.value = { name: 'Jane', profile: { age: 30 } } // ✅ 会触发更新
// shallowReactive 示例
const state = shallowReactive({
count: 0,
user: { name: 'John' }
})
state.count = 1 // ✅ 会触发更新
state.user.name = 'Jane' // ❌ 不会触发更新
state.user = { name: 'Jane' } // ✅ 会触发更新Q2: ref 的自动解包规则是什么?
A: ref 的自动解包有特定的规则:
- 模板中:总是自动解包,无需 .value
- reactive 对象中:作为属性时自动解包
- 数组和 Map 中:不会自动解包,仍需 .value
- 顶层属性:在 reactive 中作为顶层属性时解包
const count = ref(0)
const obj = reactive({
count, // 自动解包,obj.count === 0
nested: {
count // 嵌套时也会解包
}
})
// 数组中不解包
const arr = reactive([count])
console.log(arr[0].value) // 需要 .value
// Map 中不解包
const map = reactive(new Map([['count', count]]))
console.log(map.get('count').value) // 需要 .valueQ3: 如何选择 ref 还是 reactive?
A: 选择原则基于数据类型和使用方式:
- 基本类型:必须使用 ref(reactive 不支持)
- 需要整体替换:使用 ref(reactive 整体替换会丢失响应式)
- 需要解构:使用 ref 或配合 toRefs
- 复杂对象且不需要解构:使用 reactive
- API 返回的数据:通常使用 ref,便于整体替换
// 选择指南
const primitives = ref(0) // 基本类型 → ref
const apiData = ref(null) // API 数据 → ref(便于替换)
const formData = reactive({}) // 表单数据 → reactive(不需要替换)
const config = readonly(reactive({})) // 配置数据 → reactive + readonly3. watch 和 watchEffect 的区别?
难度: ⭐⭐⭐☆☆
问题: watch 和 watchEffect 有什么区别?什么时候使用 watch,什么时候使用 watchEffect?
答案:
核心区别:
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖声明 | 显式指定 | 自动追踪 |
| 首次执行 | 默认不执行(可设 immediate) | 立即执行 |
| 旧值访问 | 可获取 | 不可获取 |
| 惰性执行 | 是 | 否 |
| 使用场景 | 需要旧值、异步操作 | 简单的副作用 |
代码示例:
import { ref, watch, watchEffect, watchPostEffect } from 'vue'
const count = ref(0)
const name = ref('Vue')
// watch - 显式指定依赖
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} -> ${newVal}`)
})
// watch 多个源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log('Multiple sources changed')
})
// watch 对象属性
const state = reactive({ count: 0, nested: { value: 1 } })
watch(
() => state.count,
(newVal) => console.log(newVal)
)
// watch 选项
watch(
() => state.nested,
(newVal) => console.log(newVal),
{
immediate: true, // 立即执行
deep: true, // 深度监听
flush: 'post', // 在 DOM 更新后执行
once: true // Vue 3.4+ 只执行一次
}
)
// watchEffect - 自动追踪依赖
watchEffect(() => {
// 自动追踪回调中使用的响应式数据
console.log(`Count: ${count.value}, Name: ${name.value}`)
})
// watchPostEffect - DOM 更新后执行
watchPostEffect(() => {
// 可以安全访问更新后的 DOM
console.log(document.querySelector('#count').textContent)
})
// watchSyncEffect - 同步执行(谨慎使用)
watchSyncEffect(() => {
// 在响应式数据变化时同步执行
console.log(count.value)
})
// 停止监听
const stop = watchEffect(() => {
console.log(count.value)
})
stop() // 手动停止实际应用场景:
// 场景 1:数据请求(使用 watch)
const userId = ref(1)
watch(userId, async (newId, oldId) => {
console.log(`Fetching user ${newId}, previous: ${oldId}`)
const user = await fetchUser(newId)
// 处理用户数据
}, { immediate: true })
// 场景 2:自动保存(使用 watchEffect)
const form = reactive({
title: '',
content: ''
})
watchEffect(() => {
// 自动追踪 form 的所有属性
localStorage.setItem('draft', JSON.stringify(form))
})
// 场景 3:防抖搜索(使用 watch)
const searchQuery = ref('')
const searchResults = ref([])
watch(searchQuery, async (newQuery) => {
if (!newQuery) {
searchResults.value = []
return
}
// 防抖逻辑
await new Promise(resolve => setTimeout(resolve, 300))
searchResults.value = await search(newQuery)
})
// 场景 4:清理副作用
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log(count.value)
}, 1000)
// 清理函数
onCleanup(() => {
clearInterval(timer)
})
})追问点:
Q1: flush 选项的作用和使用场景?
A: flush 控制回调执行时机,有三个选项:
- 'pre'(默认):在组件更新前执行,适合大多数场景
- 'post':在 DOM 更新后执行,适合需要访问更新后 DOM 的场景
- 'sync':同步执行,可能影响性能,谨慎使用
// 需要访问更新后的 DOM
watch(count, (newVal) => {
// 此时 DOM 可能还没更新
console.log(document.querySelector('#count').textContent)
}, { flush: 'post' })
// 或使用 watchPostEffect
watchPostEffect(() => {
// 保证 DOM 已更新
console.log(document.querySelector('#count').textContent)
})Q2: 如何正确监听深层对象的变化?
A: 有多种方式监听深层对象:
- deep: true:监听整个对象的深层变化(性能开销大)
- 监听特定属性:使用 getter 函数监听具体属性
- 使用 JSON.stringify:监听对象序列化后的变化
const state = reactive({
user: { name: 'John', profile: { age: 25 } }
})
// 方式1:深度监听(性能开销大)
watch(state, (newVal) => {
console.log('Deep change detected')
}, { deep: true })
// 方式2:监听特定属性(推荐)
watch(() => state.user.name, (newName) => {
console.log('Name changed:', newName)
})
// 方式3:监听多个属性
watch(
() => [state.user.name, state.user.profile.age],
([newName, newAge]) => {
console.log('Name or age changed')
}
)Q3: watchEffect 的清理机制如何工作?
A: watchEffect 提供了清理机制来处理副作用:
- onCleanup 回调:在副作用重新执行前或组件卸载时调用
- 自动清理:组件卸载时自动停止监听
- 手动停止:返回停止函数,可手动停止监听
watchEffect((onCleanup) => {
// 设置副作用
const timer = setInterval(() => {
console.log('Timer tick')
}, 1000)
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
// 清理函数:在重新执行前或组件卸载时调用
onCleanup(() => {
clearInterval(timer)
controller.abort()
console.log('Cleanup executed')
})
})
// 手动停止
const stop = watchEffect(() => {
console.log(count.value)
})
// 在某个时机停止监听
onUnmounted(() => {
stop()
})- Q: watchEffect 如何获取旧值?
- A: 无法获取,需要使用 watch
4. 生命周期钩子的变化?
难度: ⭐⭐☆☆☆
问题: Vue 3 的生命周期钩子有哪些变化?Composition API 中如何使用生命周期?
答案:
生命周期对照表:
| Vue 2 | Vue 3 Options API | Vue 3 Composition API |
|---|---|---|
| beforeCreate | beforeCreate | setup() |
| created | created | setup() |
| beforeMount | beforeMount | onBeforeMount |
| mounted | mounted | onMounted |
| beforeUpdate | beforeUpdate | onBeforeUpdate |
| updated | updated | onUpdated |
| beforeDestroy | beforeUnmount | onBeforeUnmount |
| destroyed | unmounted | onUnmounted |
| activated | activated | onActivated |
| deactivated | deactivated | onDeactivated |
| errorCaptured | errorCaptured | onErrorCaptured |
| - | renderTracked | onRenderTracked |
| - | renderTriggered | onRenderTriggered |
| - | serverPrefetch | onServerPrefetch |
Composition API 使用示例:
<script setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated,
onErrorCaptured,
onRenderTracked,
onRenderTriggered
} from 'vue'
// setup() 相当于 beforeCreate + created
console.log('setup - 组件实例创建')
onBeforeMount(() => {
console.log('beforeMount - 挂载前')
})
onMounted(() => {
console.log('mounted - 挂载后,可以访问 DOM')
// 适合:数据请求、DOM 操作、第三方库初始化
})
onBeforeUpdate(() => {
console.log('beforeUpdate - 更新前')
})
onUpdated(() => {
console.log('updated - 更新后')
// 注意:避免在这里修改状态,可能导致无限循环
})
onBeforeUnmount(() => {
console.log('beforeUnmount - 卸载前')
// 适合:清理定时器、取消订阅
})
onUnmounted(() => {
console.log('unmounted - 卸载后')
})
// KeepAlive 组件专用
onActivated(() => {
console.log('activated - 组件被激活')
})
onDeactivated(() => {
console.log('deactivated - 组件被缓存')
})
// 错误捕获
onErrorCaptured((err, instance, info) => {
console.error('Error captured:', err, info)
return false // 阻止错误继续传播
})
// 调试钩子(开发环境)
onRenderTracked((e) => {
console.log('Render tracked:', e)
})
onRenderTriggered((e) => {
console.log('Render triggered:', e)
})
</script>父子组件生命周期顺序:
挂载阶段:
父 setup → 父 onBeforeMount
→ 子 setup → 子 onBeforeMount → 子 onMounted
→ 父 onMounted
更新阶段:
父 onBeforeUpdate
→ 子 onBeforeUpdate → 子 onUpdated
→ 父 onUpdated
卸载阶段:
父 onBeforeUnmount
→ 子 onBeforeUnmount → 子 onUnmounted
→ 父 onUnmounted追问点:
Q1: setup() 中为什么不能使用 this?
A: setup() 在组件实例创建之前执行,此时组件实例还未创建,this 为 undefined。设计理念是 Composition API 不依赖 this,通过参数和返回值传递数据。
Q2: 生命周期钩子可以在条件语句中使用吗?
A: 不可以,生命周期钩子必须在 setup() 的顶层同步调用,不能在异步回调、条件语句、循环中调用。解决方案是在钩子内部使用条件逻辑。
Q3: onRenderTracked 和 onRenderTriggered 的实际用途?
A: 主要用于性能调试和优化。onRenderTracked 在组件首次渲染时追踪所有响应式依赖,onRenderTriggered 在依赖变化触发重新渲染时调用,提供详细的调试信息。
5. 组件通信方式?
难度: ⭐⭐⭐☆☆
问题: Vue 3 有哪些组件通信方式?各自的适用场景是什么?
答案:
通信方式对比:
| 方式 | 适用场景 | Vue 3 变化 |
|---|---|---|
| props / emits | 父子组件 | emits 需要声明 |
| v-model | 双向绑定 | 支持多个 v-model |
| provide / inject | 跨级组件 | 支持响应式 |
| $refs | 父访问子 | 需要 defineExpose |
| $attrs | 透传属性 | 包含 class 和 style |
| mitt/tiny-emitter | 任意组件 | 替代 EventBus |
| Pinia | 全局状态 | 替代 Vuex |
1. props / emits(父子通信)
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const count = ref(0)
function handleUpdate(newCount) {
count.value = newCount
}
</script>
<template>
<Child :count="count" @update="handleUpdate" />
</template>
<!-- 子组件 -->
<script setup>
// 定义 props
const props = defineProps({
count: {
type: Number,
required: true
}
})
// 定义 emits
const emit = defineEmits(['update'])
function increment() {
emit('update', props.count + 1)
}
</script>
<template>
<button @click="increment">Count: {{ count }}</button>
</template>2. v-model(双向绑定)
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
const username = ref('')
const password = ref('')
</script>
<template>
<!-- 单个 v-model -->
<CustomInput v-model="username" />
<!-- 多个 v-model -->
<UserForm
v-model:username="username"
v-model:password="password"
/>
</template>
<!-- CustomInput.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
function handleInput(e) {
emit('update:modelValue', e.target.value)
}
</script>
<template>
<input :value="modelValue" @input="handleInput" />
</template>
<!-- UserForm.vue -->
<script setup>
defineProps(['username', 'password'])
const emit = defineEmits(['update:username', 'update:password'])
</script>
<template>
<input
:value="username"
@input="emit('update:username', $event.target.value)"
/>
<input
type="password"
:value="password"
@input="emit('update:password', $event.target.value)"
/>
</template>⭐ Vue 3.4+ 新特性:defineModel(推荐)
从 Vue 3.4 开始,defineModel 简化了 v-model 的实现,不再需要手动定义 props 和 emits。
<!-- CustomInput.vue(Vue 3.4+ 推荐写法) -->
<script setup>
// defineModel 自动处理 props 和 emits
const model = defineModel()
// 等价于:
// const props = defineProps(['modelValue'])
// const emit = defineEmits(['update:modelValue'])
// const model = computed({
// get: () => props.modelValue,
// set: (value) => emit('update:modelValue', value)
// })
</script>
<template>
<!-- 直接使用 v-model 绑定 -->
<input v-model="model" />
</template>
<!-- 使用组件 -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const username = ref('')
</script>
<template>
<CustomInput v-model="username" />
</template>多个 v-model(Vue 3.4+)
<!-- UserForm.vue -->
<script setup>
// 定义多个 model
const username = defineModel('username')
const password = defineModel('password')
// 带选项的 defineModel
const email = defineModel('email', {
type: String,
required: true,
default: '',
validator: (value) => value.includes('@')
})
</script>
<template>
<input v-model="username" placeholder="用户名" />
<input v-model="password" type="password" placeholder="密码" />
<input v-model="email" type="email" placeholder="邮箱" />
</template>
<!-- 使用组件 -->
<script setup>
import { ref } from 'vue'
import UserForm from './UserForm.vue'
const username = ref('')
const password = ref('')
const email = ref('')
</script>
<template>
<UserForm
v-model:username="username"
v-model:password="password"
v-model:email="email"
/>
</template>defineModel 修饰符(Vue 3.4+)
<!-- CustomInput.vue -->
<script setup>
// 获取 v-model 修饰符
const [model, modifiers] = defineModel({
// 设置默认修饰符
set(value) {
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
if (modifiers.trim) {
return value.trim()
}
return value
}
})
</script>
<template>
<input v-model="model" />
</template>
<!-- 使用修饰符 -->
<template>
<CustomInput v-model.capitalize="name" />
<CustomInput v-model.trim="username" />
</template>defineModel vs 传统方式对比
<!-- ❌ 传统方式(Vue 3.0-3.3) -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const model = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
</script>
<template>
<input v-model="model" />
</template>
<!-- ✅ defineModel 方式(Vue 3.4+,推荐) -->
<script setup>
const model = defineModel()
</script>
<template>
<input v-model="model" />
</template>defineModel 高级用法
<script setup>
// 1. 带类型的 defineModel(TypeScript)
const count = defineModel<number>()
// 2. 带默认值
const message = defineModel({ default: 'Hello' })
// 3. 必填项
const userId = defineModel({ required: true })
// 4. 自定义 getter/setter
const price = defineModel({
get(value) {
return value / 100 // 分转元
},
set(value) {
return value * 100 // 元转分
}
})
// 5. 本地状态(不同步到父组件)
const localValue = defineModel({ local: true })
</script>3. provide / inject(跨级通信)
<!-- 祖先组件 -->
<script setup>
import { provide, ref, readonly } from 'vue'
const theme = ref('dark')
const user = ref({ name: 'John', role: 'admin' })
// 提供响应式数据
provide('theme', theme)
// 提供只读数据(防止后代修改)
provide('user', readonly(user))
// 提供方法
provide('updateTheme', (newTheme) => {
theme.value = newTheme
})
// 使用 Symbol 作为 key(类型安全)
import { InjectionKey } from 'vue'
export const themeKey: InjectionKey<Ref<string>> = Symbol('theme')
provide(themeKey, theme)
</script>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
// 注入数据
const theme = inject('theme')
const user = inject('user')
const updateTheme = inject('updateTheme')
// 使用默认值
const config = inject('config', { timeout: 3000 })
// 使用工厂函数作为默认值
const settings = inject('settings', () => ({ mode: 'light' }), true)
function changeTheme() {
updateTheme('light')
}
</script>4. defineExpose(父访问子)
<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const inputRef = ref()
function increment() {
count.value++
}
function focus() {
inputRef.value?.focus()
}
// 暴露给父组件
defineExpose({
count,
increment,
focus
})
</script>
<template>
<input ref="inputRef" />
<button @click="increment">{{ count }}</button>
</template>
<!-- 父组件 -->
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const childRef = ref()
onMounted(() => {
console.log(childRef.value.count) // 访问子组件的 count
childRef.value.increment() // 调用子组件的方法
childRef.value.focus() // 调用子组件的方法
})
</script>
<template>
<Child ref="childRef" />
</template>5. mitt(事件总线)
// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
// 组件 A - 发送事件
import { emitter } from '@/utils/eventBus'
emitter.emit('user-login', { userId: 123 })
// 组件 B - 接收事件
import { onMounted, onUnmounted } from 'vue'
import { emitter } from '@/utils/eventBus'
onMounted(() => {
emitter.on('user-login', handleLogin)
})
onUnmounted(() => {
emitter.off('user-login', handleLogin)
})
function handleLogin(data) {
console.log('User logged in:', data.userId)
}追问点:
Q1: provide/inject 如何实现响应式?
A: 通过提供响应式对象实现:祖先组件提供 ref 或 reactive 对象,后代组件会自动响应数据变化。使用 InjectionKey 可以提供类型支持。
Q2: $attrs 包含哪些内容?
A: $attrs 包含父组件传递但未在 props 中声明的属性,包括 class、style、事件监听器等。可以通过 inheritAttrs: false 禁用自动透传。
Q3: 如何选择合适的通信方式?
A: 根据组件关系选择:父子组件用 props/emits(性能最好),跨级组件用 provide/inject(避免 prop drilling),全局状态用 Pinia(复杂状态管理)。
进阶题(重要)
6. Teleport 和 Suspense 的使用?
难度: ⭐⭐⭐⭐☆
问题: Teleport 和 Suspense 是什么?如何使用?有哪些实际应用场景?
答案:
Teleport - 传送门组件: 将组件的 DOM 渲染到指定位置,常用于模态框、通知、下拉菜单等需要脱离父组件层级的场景。
基础用法:
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>
<template>
<div class="app">
<button @click="showModal = true">打开模态框</button>
<!-- 将模态框渲染到 body 下 -->
<Teleport to="body">
<div v-if="showModal" class="modal-overlay" @click="showModal = false">
<div class="modal-content" @click.stop>
<h2>模态框标题</h2>
<p>这个内容被渲染到 body 元素下</p>
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
</div>
</template>
<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-content {
background: white;
padding: 2rem;
border-radius: 8px;
max-width: 500px;
}
</style>Teleport 高级用法:
<!-- 多个 Teleport 到同一目标 -->
<Teleport to="#modals">
<div class="modal">Modal 1</div>
</Teleport>
<Teleport to="#modals">
<div class="modal">Modal 2</div>
</Teleport>
<!-- 条件禁用 Teleport -->
<Teleport to="body" :disabled="isMobile">
<div class="tooltip">
<!-- 移动端不传送,桌面端传送到 body -->
</div>
</Teleport>
<!-- 延迟传送(等待目标元素存在) -->
<Teleport to="#late-div" :defer="true">
<div>Deferred content</div>
</Teleport>Suspense - 异步组件加载: 处理异步操作的加载状态,支持异步组件和异步 setup。
基础用法:
<script setup>
import { defineAsyncComponent } from 'vue'
// 异步组件
const AsyncComp = defineAsyncComponent(() =>
import('./AsyncComponent.vue')
)
</script>
<template>
<Suspense>
<!-- 异步组件 -->
<template #default>
<AsyncComp />
</template>
<!-- 加载中状态 -->
<template #fallback>
<div class="loading">
<span>加载中...</span>
</div>
</template>
</Suspense>
</template>异步 setup:
<!-- UserProfile.vue -->
<script setup>
// 可以直接使用 await,Suspense 会处理加载状态
const props = defineProps(['userId'])
const user = await fetch(`/api/users/${props.userId}`).then(r => r.json())
const posts = await fetch(`/api/users/${props.userId}/posts`).then(r => r.json())
</script>
<template>
<div class="user-profile">
<h2>{{ user.name }}</h2>
<ul>
<li v-for="post in posts" :key="post.id">
{{ post.title }}
</li>
</ul>
</div>
</template>
<!-- 父组件 -->
<template>
<Suspense>
<UserProfile :user-id="123" />
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>错误处理:
<script setup>
import { ref, onErrorCaptured } from 'vue'
const error = ref(null)
onErrorCaptured((err) => {
error.value = err
return false // 阻止错误继续传播
})
</script>
<template>
<div v-if="error" class="error">
<p>加载失败: {{ error.message }}</p>
<button @click="error = null">重试</button>
</div>
<Suspense v-else>
<AsyncComponent />
<template #fallback>
<Loading />
</template>
</Suspense>
</template>嵌套 Suspense:
<template>
<Suspense>
<template #default>
<Dashboard>
<!-- 嵌套的 Suspense -->
<Suspense>
<UserWidget />
<template #fallback>
<WidgetSkeleton />
</template>
</Suspense>
<Suspense>
<StatsWidget />
<template #fallback>
<WidgetSkeleton />
</template>
</Suspense>
</Dashboard>
</template>
<template #fallback>
<PageSkeleton />
</template>
</Suspense>
</template>追问点:
Q1: Suspense 和 v-if 的区别?
A: Suspense 专门处理异步加载状态,自动检测异步组件的加载状态;v-if 只是基于布尔值的条件渲染,需要手动管理加载状态。
Q2: Teleport 如何影响事件冒泡?
A: Teleport 不影响事件冒泡机制,事件仍然按照组件树冒泡,而不是 DOM 树。这保持了组件的逻辑关系不变。
Q3: 如何实现复杂的异步加载状态?
A: 可以监听 Suspense 的 @pending、@resolve、@fallback 事件,在 fallback 插槽中实现骨架屏、进度条等复杂的加载状态展示。
7. 性能优化最佳实践?
难度: ⭐⭐⭐⭐☆
问题: Vue 3 有哪些性能优化方法?如何优化大列表渲染?
答案:
优化方法总览:
1. v-memo(Vue 3.2+) 缓存子树,只有依赖变化时才重新渲染。
<template>
<div v-for="item in list" :key="item.id">
<!-- 只有 item.id 或 selected 变化时才重新渲染 -->
<div v-memo="[item.id, selected]">
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
<button @click="select(item.id)">
{{ selected === item.id ? '已选中' : '选择' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const list = ref([/* 大量数据 */])
const selected = ref(null)
function select(id) {
selected.value = id
}
</script>2. shallowRef 和 shallowReactive 只有根级别是响应式的,减少响应式开销。
import { shallowRef, shallowReactive, triggerRef } from 'vue'
// shallowRef - 只有 .value 的变化是响应式的
const state = shallowRef({
count: 0,
nested: { value: 1 }
})
// 不会触发更新
state.value.count++
// 会触发更新
state.value = { count: 1, nested: { value: 2 } }
// 手动触发更新
state.value.count++
triggerRef(state)
// shallowReactive - 只有根级别属性是响应式的
const state2 = shallowReactive({
count: 0,
nested: { value: 1 }
})
// 会触发更新
state2.count++
// 不会触发更新
state2.nested.value++3. 虚拟列表(大数据渲染)
<script setup>
import { ref, computed } from 'vue'
const items = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i}`
})))
const containerHeight = 600
const itemHeight = 50
const visibleCount = Math.ceil(containerHeight / itemHeight)
const scrollTop = ref(0)
const startIndex = computed(() =>
Math.floor(scrollTop.value / itemHeight)
)
const endIndex = computed(() =>
Math.min(startIndex.value + visibleCount + 1, items.value.length)
)
const visibleItems = computed(() =>
items.value.slice(startIndex.value, endIndex.value)
)
const offsetY = computed(() =>
startIndex.value * itemHeight
)
const totalHeight = computed(() =>
items.value.length * itemHeight
)
function handleScroll(e) {
scrollTop.value = e.target.scrollTop
}
</script>
<template>
<div
class="virtual-list"
:style="{ height: containerHeight + 'px' }"
@scroll="handleScroll"
>
<div :style="{ height: totalHeight + 'px', position: 'relative' }">
<div :style="{ transform: `translateY(${offsetY}px)` }">
<div
v-for="item in visibleItems"
:key="item.id"
:style="{ height: itemHeight + 'px' }"
class="item"
>
{{ item.text }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.virtual-list {
overflow-y: auto;
border: 1px solid #ccc;
}
.item {
display: flex;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid #eee;
}
</style>4. 组件懒加载
import { defineAsyncComponent } from 'vue'
// 基础用法
const AsyncComp = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
// 带选项
const AsyncCompWithOptions = defineAsyncComponent({
loader: () => import('./components/AsyncComponent.vue'),
// 加载中显示的组件
loadingComponent: LoadingSpinner,
// 加载失败显示的组件
errorComponent: ErrorComponent,
// 延迟显示加载组件的时间(默认 200ms)
delay: 200,
// 超时时间
timeout: 3000,
// 是否挂起(配合 Suspense 使用)
suspensible: false,
// 错误处理
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
retry() // 重试
} else {
fail() // 失败
}
}
})5. KeepAlive 缓存优化
<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const cachedViews = ref(['Home', 'UserList', 'ProductList'])
// 动态控制缓存
watch(() => route.meta.keepAlive, (shouldCache) => {
const componentName = route.name
if (shouldCache && !cachedViews.value.includes(componentName)) {
cachedViews.value.push(componentName)
} else if (!shouldCache) {
const index = cachedViews.value.indexOf(componentName)
if (index > -1) {
cachedViews.value.splice(index, 1)
}
}
})
</script>
<template>
<RouterView v-slot="{ Component }">
<KeepAlive :include="cachedViews" :max="10">
<component :is="Component" :key="$route.fullPath" />
</KeepAlive>
</RouterView>
</template>6. 计算属性缓存
import { ref, computed } from 'vue'
const items = ref([/* 大量数据 */])
const filter = ref('')
// ✅ 好:使用 computed 缓存
const filteredItems = computed(() => {
console.log('Filtering...') // 只在依赖变化时执行
return items.value.filter(item =>
item.name.includes(filter.value)
)
})
// ❌ 不好:每次渲染都计算
function getFilteredItems() {
console.log('Filtering...') // 每次渲染都执行
return items.value.filter(item =>
item.name.includes(filter.value)
)
}性能对比:
| 优化方法 | 适用场景 | 性能提升 |
|---|---|---|
| v-memo | 大列表、复杂子树 | 30-50% |
| shallowRef/Reactive | 大对象、不需要深层响应式 | 20-40% |
| 虚拟列表 | 10000+ 条数据 | 90%+ |
| 懒加载 | 大型组件、路由 | 首屏 50%+ |
| KeepAlive | 频繁切换的组件 | 避免重复渲染 |
追问点:
Q1: v-once 和 v-memo 的区别?
A: v-once 只渲染一次后永不更新,适合静态内容;v-memo 根据依赖数组决定是否重新渲染,适合昂贵的计算。v-memo 更灵活,v-once 性能更极致。
Q2: 虚拟列表的局限性?
A: 需要固定或可预测的高度,不支持复杂布局。对于高度不一致的列表项,需要额外的高度计算和缓存机制。
Q3: 如何监控 Vue 应用的性能?
A: 使用 Vue DevTools 的 Performance 面板监控组件渲染时间,使用浏览器的 Performance API 监控页面性能,结合 Lighthouse 进行综合性能评估。
8. Composables 最佳实践?
难度: ⭐⭐⭐⭐☆
问题: 什么是 Composables?如何编写高质量的 Composables?
答案:
Composables 定义:
- 使用 Composition API 封装可复用逻辑的函数
- 命名约定:以
use开头 - 可以使用响应式 API、生命周期钩子等
1. 鼠标位置追踪
// composables/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 }
}
// 使用
<script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<div>鼠标位置: {{ x }}, {{ y }}</div>
</template>2. 数据请求封装
// composables/useFetch.ts
import { ref, unref, watchEffect, toValue, type Ref } from 'vue'
export function useFetch<T>(url: string | Ref<string>) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(false)
watchEffect(async () => {
loading.value = true
error.value = null
data.value = null
try {
// toValue() 在 3.3+ 中可用,会自动解包 ref
const urlValue = toValue(url)
const res = await fetch(urlValue)
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`)
}
data.value = await res.json()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
})
return { data, error, loading }
}
// 使用
<script setup>
import { ref, computed } from 'vue'
import { useFetch } from '@/composables/useFetch'
const userId = ref(1)
const url = computed(() => `/api/users/${userId.value}`)
const { data: user, loading, error } = useFetch(url)
</script>
<template>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error.message }}</div>
<div v-else-if="user">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
</template>3. 本地存储同步
// composables/useLocalStorage.ts
import { ref, watch, type Ref } from 'vue'
export function useLocalStorage<T>(
key: string,
defaultValue: T
): Ref<T> {
const data = ref<T>(defaultValue)
// 从 localStorage 读取初始值
const stored = localStorage.getItem(key)
if (stored) {
try {
data.value = JSON.parse(stored)
} catch (e) {
console.error('Failed to parse localStorage value:', e)
}
}
// 监听变化并同步到 localStorage
watch(
data,
(newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
},
{ deep: true }
)
return data as Ref<T>
}
// 使用
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const theme = useLocalStorage('theme', 'light')
const userSettings = useLocalStorage('settings', {
notifications: true,
language: 'zh-CN'
})
</script>4. 防抖和节流
// composables/useDebounce.ts
import { ref, watch, type Ref } from 'vue'
export function useDebounce<T>(value: Ref<T>, delay = 300): Ref<T> {
const debouncedValue = ref(value.value) as Ref<T>
let timeout: ReturnType<typeof setTimeout>
watch(value, (newValue) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
debouncedValue.value = newValue
}, delay)
})
return debouncedValue
}
// composables/useThrottle.ts
export function useThrottle<T>(value: Ref<T>, delay = 300): Ref<T> {
const throttledValue = ref(value.value) as Ref<T>
let lastUpdate = 0
watch(value, (newValue) => {
const now = Date.now()
if (now - lastUpdate >= delay) {
throttledValue.value = newValue
lastUpdate = now
}
})
return throttledValue
}
// 使用
<script setup>
import { ref, watch } from 'vue'
import { useDebounce } from '@/composables/useDebounce'
const searchQuery = ref('')
const debouncedQuery = useDebounce(searchQuery, 500)
// 只有当用户停止输入 500ms 后才会触发搜索
watch(debouncedQuery, (query) => {
performSearch(query)
})
</script>5. 网络状态监听
// composables/useOnline.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useOnline() {
const isOnline = ref(navigator.onLine)
function updateOnlineStatus() {
isOnline.value = navigator.onLine
}
onMounted(() => {
window.addEventListener('online', updateOnlineStatus)
window.addEventListener('offline', updateOnlineStatus)
})
onUnmounted(() => {
window.removeEventListener('online', updateOnlineStatus)
window.removeEventListener('offline', updateOnlineStatus)
})
return { isOnline }
}
// 使用
<script setup>
import { useOnline } from '@/composables/useOnline'
const { isOnline } = useOnline()
</script>
<template>
<div v-if="!isOnline" class="offline-banner">
您当前处于离线状态
</div>
</template>Composables 最佳实践:
- 命名约定:以
use开头 - 返回值:返回 ref 或 reactive 对象
- 清理副作用:在 onUnmounted 中清理
- 参数灵活:支持 ref 和普通值
- 类型安全:使用 TypeScript
- 单一职责:每个 composable 只做一件事
追问点:
Q1: Composables 和 Mixins 的区别?
A: Composables 更灵活、无命名冲突、类型推断更好。通过解构避免属性覆盖,明确数据来源,支持参数化和完整的 TypeScript 类型推断。
Q2: 如何测试 Composables?
A: 使用 @vue/test-utils 创建测试组件,或者直接在测试环境中调用 composable 函数。可以独立测试逻辑,不依赖具体的组件实例。
Q3: Composables 可以嵌套使用吗?
A: 可以,一个 composable 可以调用其他 composables,形成组合式的逻辑复用。这种组合方式比 Mixins 更清晰和可维护。
高级题(加分)
9. Vue 3 响应式原理?
难度: ⭐⭐⭐⭐⭐
问题: Vue 3 的响应式系统是如何实现的?Proxy 相比 Object.defineProperty 有什么优势?
答案:
Proxy 实现原理:
// 简化的 reactive 实现
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
// 依赖收集
track(target, key)
// 如果是对象,递归代理(惰性代理)
if (typeof result === 'object' && result !== null) {
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
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
trigger(target, key)
return result
}
})
}
// 依赖收集
const targetMap = new WeakMap()
let activeEffect = null
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
// effect 函数
function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}Proxy vs Object.defineProperty 对比:
| 特性 | Vue 2 (Object.defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 对象属性添加 | 需要 Vue.set | 自动检测 ✅ |
| 对象属性删除 | 需要 Vue.delete | 自动检测 ✅ |
| 数组索引修改 | 需要 Vue.set | 自动检测 ✅ |
| 数组长度修改 | 不支持 | 支持 ✅ |
| Map/Set 支持 | 不支持 | 支持 ✅ |
| 性能 | 初始化时递归遍历 | 惰性代理 ✅ |
| 浏览器兼容 | IE9+ | 不支持 IE |
代码对比:
// Vue 2 的限制
const obj = { count: 0 }
obj.newProp = 1 // ❌ 不会触发更新
Vue.set(obj, 'newProp', 1) // ✅ 需要使用 Vue.set
const arr = [1, 2, 3]
arr[0] = 10 // ❌ 不会触发更新
Vue.set(arr, 0, 10) // ✅ 需要使用 Vue.set
// Vue 3 自动检测
const state = reactive({ count: 0 })
state.newProp = 1 // ✅ 自动触发更新
const arr = reactive([1, 2, 3])
arr[0] = 10 // ✅ 自动触发更新
arr.length = 0 // ✅ 自动触发更新
// 支持 Map 和 Set
const map = reactive(new Map())
map.set('key', 'value') // ✅ 响应式
const set = reactive(new Set())
set.add(1) // ✅ 响应式追问点:
Q1: Proxy 相比 Object.defineProperty 的性能优势?
A: Proxy 初始化更快(惰性代理),可以拦截动态属性和数组操作,但访问时有轻微开销。整体性能优于 Vue 2 的递归遍历方式。
Q2: 如何处理 Proxy 不支持的浏览器?
A: Vue 3 完全放弃了 IE11 支持,因为 Proxy 无法被 polyfill。如需支持旧浏览器,只能继续使用 Vue 2。
Q3: ref 的实现原理?
A: ref 使用 getter/setter 包装对象,通过 .value 访问值。在模板中会自动解包,在 reactive 对象中也会自动解包。
10. 编译优化原理?
难度: ⭐⭐⭐⭐⭐
问题: Vue 3 的编译器做了哪些优化?什么是 PatchFlags 和 Block Tree?
答案:
编译优化策略:
1. 静态提升 (hoistStatic)
<template>
<div>
<p>Static text</p> <!-- 静态节点 -->
<p>{{ dynamic }}</p> <!-- 动态节点 -->
</div>
</template>编译后:
// 静态节点被提升到渲染函数外
const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, "Static text")
function render(_ctx) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1, // 复用静态节点
_createElementVNode("p", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
]))
}2. PatchFlags 标记
// 标记类型
export const enum PatchFlags {
TEXT = 1, // 动态文本
CLASS = 1 << 1, // 动态 class
STYLE = 1 << 2, // 动态 style
PROPS = 1 << 3, // 动态 props
FULL_PROPS = 1 << 4,// 有动态 key 的 props
HYDRATE_EVENTS = 1 << 5,
STABLE_FRAGMENT = 1 << 6,
KEYED_FRAGMENT = 1 << 7,
UNKEYED_FRAGMENT = 1 << 8,
NEED_PATCH = 1 << 9,
DYNAMIC_SLOTS = 1 << 10,
HOISTED = -1,
BAIL = -2
}
// 编译示例
<div :class="className">{{ text }}</div>
// 编译后
_createElementVNode("div", {
class: _ctx.className
}, _toDisplayString(_ctx.text), 3 /* TEXT, CLASS */)3. Block Tree
- 将动态节点收集到 Block 中
- 更新时只对比 Block 内的动态节点
- 跳过静态内容的对比
// Block 示例
<div>
<p>Static</p>
<p>{{ dynamic1 }}</p>
<p>Static</p>
<p>{{ dynamic2 }}</p>
</div>
// 编译后的 Block
{
type: 'div',
dynamicChildren: [
{ type: 'p', children: dynamic1, patchFlag: TEXT },
{ type: 'p', children: dynamic2, patchFlag: TEXT }
]
}4. 缓存事件处理器
<template>
<button @click="handleClick">Click</button>
</template>
<!-- 编译后 -->
<script>
// 事件处理器被缓存
const _cache = []
_cache[0] = _ctx.handleClick
</script>性能提升:
- 静态提升:减少创建 VNode 的开销
- PatchFlags:精确更新,跳过不必要的对比
- Block Tree:减少遍历节点数量
- 事件缓存:避免重复创建函数
追问点:
Q1: 如何查看和分析编译结果?
A: 有多种方式查看 Vue 编译结果:
- Vue SFC Playground:在线查看编译结果和优化效果
- @vue/compiler-sfc:本地编译分析,可以看到详细的编译选项
- Vite 插件:使用 vite-plugin-vue 的调试选项
- 编译器选项:通过 compilerOptions 控制编译行为
// 本地分析编译结果
import { compileTemplate } from '@vue/compiler-sfc'
const result = compileTemplate({
source: '<div>{{ count }}</div>',
filename: 'test.vue'
})
console.log(result.code) // 查看编译后的代码Q2: v-once 和静态提升的区别?
A: 两者都是性能优化,但机制不同:
- v-once:运行时优化,首次渲染后跳过后续更新
- 静态提升:编译时优化,将静态元素提升到渲染函数外部
- 使用场景:v-once 适合动态变静态的内容,静态提升适合完全静态的内容
- 性能影响:静态提升避免重复创建,v-once 避免重复渲染
Q3: 如何控制编译优化的行为?
A: 通过 compilerOptions 精确控制:
- hoistStatic:控制静态提升
- cacheHandlers:控制事件处理器缓存
- prefixIdentifiers:控制标识符前缀
- optimizeImports:控制导入优化
// Vue CLI 配置
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.compilerOptions = {
hoistStatic: false, // 禁用静态提升
cacheHandlers: false, // 禁用事件缓存
}
return options
})
}
}场景题(实战)
11. 组件状态丢失问题?
难度: ⭐⭐⭐☆☆
问题: 列表拖拽排序后,组件的内部状态(如输入框的值)错位了,如何解决?
答案:
问题原因:
- 使用数组索引作为 key
- 拖拽后索引改变,Vue 复用了错误的组件实例
错误示例:
<script setup>
import { ref } from 'vue'
const todos = ref([
{ text: 'Learn Vue', done: false },
{ text: 'Build App', done: false },
])
function moveTodo(from, to) {
const [moved] = todos.value.splice(from, 1)
todos.value.splice(to, 0, moved)
}
</script>
<template>
<div v-for="(todo, index) in todos" :key="index">
<!-- ❌ 错误:使用索引作为 key -->
<input v-model="todo.text" />
</div>
</template>正确示例:
<script setup>
import { ref } from 'vue'
const todos = ref([
{ id: 1, text: 'Learn Vue', done: false },
{ id: 2, text: 'Build App', done: false },
])
function moveTodo(from, to) {
const [moved] = todos.value.splice(from, 1)
todos.value.splice(to, 0, moved)
}
</script>
<template>
<div v-for="todo in todos" :key="todo.id">
<!-- ✅ 正确:使用唯一 ID -->
<input v-model="todo.text" />
</div>
</template>受控组件方案:
<script setup>
import { ref } from 'vue'
const todos = ref([
{ id: 1, text: 'Learn Vue', editing: false },
{ id: 2, text: 'Build App', editing: false },
])
function updateTodo(id, updates) {
const todo = todos.value.find(t => t.id === id)
if (todo) {
Object.assign(todo, updates)
}
}
</script>
<template>
<div v-for="todo in todos" :key="todo.id">
<input
v-if="todo.editing"
:value="todo.text"
@input="updateTodo(todo.id, { text: $event.target.value })"
/>
<span v-else @click="updateTodo(todo.id, { editing: true })">
{{ todo.text }}
</span>
</div>
</template>追问点:
Q1: 什么时候需要重置组件状态?
A: 当组件的 key 改变时,Vue 会卸载旧组件并挂载新组件:
- 数据变化:当列表项的唯一标识发生变化时
- 强制重置:需要清空组件内部状态时
- 路由切换:相同组件但需要重置状态时
- 条件渲染:v-if 切换时也会重置状态
// 强制重置组件状态
const resetKey = ref(0)
const forceReset = () => {
resetKey.value++
}
// 模板中使用
<MyComponent :key="resetKey" />Q2: 如何选择合适的 key 值?
A: key 的选择原则:
- 唯一性:在同一列表中必须唯一
- 稳定性:相同数据应该有相同的 key
- 简单性:避免复杂的计算,影响性能
- 避免索引:数组索引不适合作为 key(除非列表不会变化)
// ✅ 好的 key 选择
items.map(item => ({
key: item.id, // 数据库 ID
key: item.uuid, // UUID
key: `${item.type}-${item.id}` // 组合 key
}))
// ❌ 不好的 key 选择
items.map((item, index) => ({
key: index, // 数组索引
key: Math.random(), // 随机数
key: new Date().getTime() // 时间戳
}))Q3: 虚拟列表如何处理 key 和状态?
A: 虚拟列表的特殊处理:
- itemKey 函数:指定如何从数据生成 key
- 状态外置:将组件状态提升到父组件或全局状态
- 缓存策略:缓存组件实例或状态数据
- 滚动位置:保持滚动位置和选中状态
// 虚拟列表 key 处理
const virtualListProps = {
itemKey: (item) => item.id,
itemSize: 50,
items: list.value
}
// 状态外置到父组件
const itemStates = reactive(new Map())
const getItemState = (id) => itemStates.get(id) || {}
const setItemState = (id, state) => itemStates.set(id, state)12. 内存泄漏排查?
难度: ⭐⭐⭐⭐☆
问题: 组件卸载后内存没有释放,如何排查和解决内存泄漏?
答案:
常见原因:
1. 定时器未清理
<script setup>
import { onMounted, onUnmounted } from 'vue'
let timer
onMounted(() => {
// ❌ 错误:定时器未清理
timer = setInterval(() => {
console.log('tick')
}, 1000)
})
// ✅ 正确:清理定时器
onUnmounted(() => {
clearInterval(timer)
})
</script>2. 事件监听器未移除
<script setup>
import { onMounted, onUnmounted } from 'vue'
function handleResize() {
console.log('resize')
}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
// ✅ 清理事件监听器
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>3. 第三方库实例未销毁
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import ECharts from 'echarts'
const chartRef = ref()
let chartInstance
onMounted(() => {
chartInstance = ECharts.init(chartRef.value)
chartInstance.setOption({/* ... */})
})
// ✅ 销毁图表实例
onUnmounted(() => {
chartInstance?.dispose()
})
</script>
<template>
<div ref="chartRef" style="width: 600px; height: 400px"></div>
</template>4. 全局状态未清理
// ❌ 错误:组件卸载后仍持有引用
const globalStore = {
listeners: []
}
export default {
setup() {
const listener = () => console.log('event')
globalStore.listeners.push(listener)
// 忘记清理
}
}
// ✅ 正确:清理全局引用
export default {
setup() {
const listener = () => console.log('event')
globalStore.listeners.push(listener)
onUnmounted(() => {
const index = globalStore.listeners.indexOf(listener)
if (index > -1) {
globalStore.listeners.splice(index, 1)
}
})
}
}5. 闭包引用大对象
<script setup>
import { ref, watch } from 'vue'
const largeData = ref(new Array(1000000).fill(0))
// ❌ 错误:闭包持有 largeData 引用
watch(someValue, () => {
console.log(largeData.value.length)
})
// ✅ 正确:只引用需要的数据
const dataLength = computed(() => largeData.value.length)
watch(someValue, () => {
console.log(dataLength.value)
})
</script>排查工具:
Chrome DevTools Memory Profiler
- 拍摄堆快照
- 对比组件挂载前后的内存
- 查找 Detached DOM 节点
Vue DevTools
- 查看组件树
- 检查是否有未卸载的组件
Performance Monitor
- 监控内存使用趋势
- 查看 JS Heap Size
追问点:
Q1: 如何系统性地检测内存泄漏?
A: 使用多种工具和方法检测:
- Chrome DevTools:Memory 面板录制堆快照,对比前后差异
- 重复测试:重复挂载/卸载组件,观察内存是否持续增长
- Performance 面板:查看 JS Heap Size 变化趋势
- 自动化测试:编写测试脚本自动检测内存泄漏
// 内存泄漏检测脚本
async function detectMemoryLeak() {
const initialMemory = performance.memory.usedJSHeapSize
// 重复挂载/卸载组件 100 次
for (let i = 0; i < 100; i++) {
const app = createApp(TestComponent)
const container = document.createElement('div')
app.mount(container)
app.unmount()
// 强制垃圾回收(仅在开发环境)
if (window.gc) window.gc()
}
const finalMemory = performance.memory.usedJSHeapSize
const leakSize = finalMemory - initialMemory
if (leakSize > 1024 * 1024) { // 超过 1MB
console.warn(`Potential memory leak: ${leakSize} bytes`)
}
}Q2: WeakMap 和 Map 在内存管理上的区别?
A: WeakMap 提供更好的内存管理:
- 弱引用:WeakMap 的 key 是弱引用,不阻止垃圾回收
- 自动清理:当 key 对象被回收时,对应的条目自动删除
- 无法遍历:WeakMap 不可遍历,没有 size 属性
- 使用场景:适合存储对象的私有数据或缓存
// Map - 强引用,可能导致内存泄漏
const cache = new Map()
function processData(obj) {
cache.set(obj, expensiveComputation(obj))
// obj 永远不会被垃圾回收
}
// WeakMap - 弱引用,自动清理
const cache = new WeakMap()
function processData(obj) {
cache.set(obj, expensiveComputation(obj))
// obj 可以被垃圾回收,缓存自动清理
}Q3: 如何避免 Vue 组件中的闭包陷阱?
A: 闭包陷阱的预防策略:
- 最小化引用:在 watch/computed 中只引用必要的数据
- 使用 toRef:避免引用整个响应式对象
- 及时清理:在 onUnmounted 中清理引用
- 避免循环引用:注意父子组件间的相互引用
// ❌ 闭包陷阱:引用了整个对象
const largeData = reactive({ /* 大量数据 */ })
watch(() => largeData.someProperty, () => {
// 整个 largeData 被闭包捕获
})
// ✅ 正确做法:只引用需要的属性
const someProperty = toRef(largeData, 'someProperty')
watch(someProperty, () => {
// 只捕获需要的数据
})
// ✅ 或者使用 getter 函数
watch(
() => largeData.someProperty,
() => { /* ... */ },
{ flush: 'post' }
)13. SSR 水合不匹配?
难度: ⭐⭐⭐⭐☆
问题: 服务端渲染后,客户端水合时出现警告:Hydration mismatch,如何解决?
答案:
常见原因:
1. 使用浏览器 API
<script setup>
import { ref, onMounted } from 'vue'
// ❌ 错误:服务端没有 window
const width = ref(window.innerWidth)
// ✅ 正确:只在客户端获取
const width = ref(0)
onMounted(() => {
width.value = window.innerWidth
})
</script>2. 随机数或时间戳
<script setup>
import { ref, onMounted } from 'vue'
// ❌ 错误:服务端和客户端值不同
const id = ref(Math.random())
const time = ref(Date.now())
// ✅ 正确:使用固定值或在客户端生成
const id = ref(null)
onMounted(() => {
id.value = Math.random()
})
</script>3. 第三方库渲染不一致
<script setup>
import { ref, onMounted } from 'vue'
const isClient = ref(false)
onMounted(() => {
isClient.value = true
})
</script>
<template>
<!-- 服务端不渲染,客户端才渲染 -->
<ClientOnly>
<ThirdPartyComponent />
</ClientOnly>
<!-- 或使用条件渲染 -->
<ThirdPartyComponent v-if="isClient" />
</template>4. 条件渲染差异
<script setup>
import { ref } from 'vue'
// ❌ 错误:服务端和客户端条件不同
const isMobile = ref(window.innerWidth < 768)
// ✅ 正确:使用 User-Agent 或统一的初始值
const isMobile = ref(false) // 服务端默认 false
onMounted(() => {
isMobile.value = window.innerWidth < 768
})
</script>解决方案:
1. 使用 ClientOnly 组件
<template>
<div>
<h1>{{ title }}</h1>
<!-- 只在客户端渲染 -->
<ClientOnly>
<BrowserOnlyComponent />
<template #fallback>
<div>Loading...</div>
</template>
</ClientOnly>
</div>
</template>2. 检查环境
// 检查是否在浏览器环境
if (typeof window !== 'undefined') {
// 浏览器代码
}
// 或使用 import.meta.env.SSR
if (!import.meta.env.SSR) {
// 客户端代码
}3. 统一初始状态
<script setup>
import { ref, onMounted } from 'vue'
// 服务端和客户端使用相同的初始值
const data = ref(null)
onMounted(async () => {
// 客户端再获取数据
data.value = await fetchData()
})
</script>追问点:
Q1: 什么是水合(Hydration)过程?
A: 水合是 SSR 的关键步骤:
- 接管 HTML:客户端 JavaScript 接管服务端渲染的静态 HTML
- 添加交互:为 DOM 元素添加事件监听器和响应式数据绑定
- 状态同步:确保客户端状态与服务端渲染时的状态一致
- 激活组件:将静态 HTML 转换为可交互的 Vue 组件
// SSR 水合过程
// 1. 服务端渲染生成 HTML
const html = renderToString(app)
// 2. 客户端水合
import { createSSRApp } from 'vue'
const app = createSSRApp(App)
app.mount('#app') // 水合现有的 HTML,而不是替换Q2: 如何系统性地调试水合不匹配?
A: 调试水合不匹配的方法:
- 开启详细警告:设置
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__为 true - 对比 HTML:使用浏览器开发工具对比服务端和客户端的 HTML 结构
- 分段调试:逐步注释组件,定位问题组件
- 日志记录:在服务端和客户端记录关键数据的值
// 开启详细的水合错误信息
app.config.warnHandler = (msg, instance, trace) => {
if (msg.includes('Hydration')) {
console.error('Hydration error:', msg)
console.error('Component:', instance)
console.error('Trace:', trace)
}
}
// 调试特定数据
const debugData = (data, label) => {
console.log(`[${typeof window === 'undefined' ? 'Server' : 'Client'}] ${label}:`, data)
}Q3: 水合失败的后果和预防措施?
A: 水合失败的影响和预防:
- 性能损失:Vue 会销毁服务端 HTML 并重新渲染,失去 SSR 的首屏优势
- 用户体验:可能出现页面闪烁或内容跳动
- SEO 影响:搜索引擎看到的内容与用户最终看到的可能不一致
预防措施:
// 1. 使用 ClientOnly 组件包装客户端特有内容
<ClientOnly>
<UserAgent />
</ClientOnly>
// 2. 延迟初始化客户端特有数据
const isClient = ref(false)
onMounted(() => {
isClient.value = true
})
// 3. 使用环境变量区分服务端和客户端逻辑
const timestamp = import.meta.env.SSR
? '2024-01-01'
: new Date().toISOString()Vue 3.4+ 新特性(重要)
1. defineModel 宏(Vue 3.4+)
难度: ⭐⭐⭐☆☆
问题: Vue 3.4 引入的 defineModel 有什么优势?如何使用?
答案:
defineModel 优势:
- 简化代码:不需要手动定义 props 和 emits
- 更直观:直接使用 v-model 绑定
- 类型安全:更好的 TypeScript 支持
- 减少样板代码:自动处理双向绑定逻辑
基础用法:
<!-- 子组件 -->
<script setup>
// ✅ Vue 3.4+ 推荐写法
const model = defineModel()
</script>
<template>
<input v-model="model" />
</template>
<!-- 父组件 -->
<template>
<CustomInput v-model="username" />
</template>多个 v-model:
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>
<template>
<input v-model="firstName" />
<input v-model="lastName" />
</template>带选项的 defineModel:
<script setup>
const count = defineModel({
type: Number,
required: true,
default: 0,
validator: (value) => value >= 0
})
// TypeScript 类型
const message = defineModel<string>()
</script>追问点:
Q1: defineModel 相比传统方式的优势?
A: defineModel 显著简化了 v-model 的实现,从 10+ 行代码减少到 1 行,自动处理 props 和 emits,提供完整的 TypeScript 支持。
Q2: defineModel 如何处理修饰符?
A: 支持通过第二个参数获取修饰符,可以在 get/set 中处理修饰符逻辑,如 trim、number 等内置修饰符。
Q3: 如何在 defineModel 中自定义逻辑?
A: 使用 get 和 set 选项自定义 getter/setter,可以实现数据转换、验证、格式化等复杂逻辑。
2. 泛型组件(Vue 3.3+)
难度: ⭐⭐⭐⭐☆
问题: Vue 3.3 引入的泛型组件有什么用途?如何实现?
答案:
泛型组件用途:
- 类型安全:保持数据类型的一致性
- 代码复用:同一组件支持多种类型
- 更好的 IDE 支持:自动补全和类型检查
基础用法:
<!-- GenericList.vue -->
<script setup lang="ts" generic="T">
defineProps<{
items: T[]
keyField: keyof T
}>()
defineEmits<{
select: [item: T]
}>()
</script>
<template>
<div v-for="item in items" :key="item[keyField]">
<slot :item="item" />
</div>
</template>
<!-- 使用 -->
<script setup lang="ts">
interface User {
id: number
name: string
}
const users: User[] = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]
</script>
<template>
<GenericList :items="users" key-field="id" @select="handleSelect">
<template #default="{ item }">
<!-- item 的类型是 User,有完整的类型提示 -->
<div>{{ item.name }}</div>
</template>
</GenericList>
</template>多个泛型参数:
<script setup lang="ts" generic="T, K extends keyof T">
defineProps<{
data: T
field: K
}>()
// K 被约束为 T 的键
</script>泛型约束:
<script setup lang="ts" generic="T extends { id: number }">
// T 必须包含 id 属性
defineProps<{
items: T[]
}>()
</script>追问点:
Q1: 泛型组件相比普通组件的优势?
A: 泛型组件可以保持类型信息,提供更好的类型安全、智能提示和编译时检查,一个组件可以处理多种类型的数据。
Q2: 如何在泛型组件中使用其他 Vue 3.3+ 特性?
A: 可以结合使用 defineModel、defineSlots、defineEmits 等,都支持泛型,提供完整的类型安全体验。
Q3: 泛型组件的性能影响?
A: 仅在编译时有影响,运行时无性能损失。泛型信息在编译后会被擦除,不会增加运行时开销。
3. defineSlots 宏(Vue 3.3+)
难度: ⭐⭐⭐☆☆
问题: defineSlots 有什么作用?如何使用?
答案:
defineSlots 作用:
- 类型安全:为插槽提供类型定义
- IDE 支持:更好的自动补全
- 文档化:明确插槽的参数类型
基础用法:
<!-- Card.vue -->
<script setup lang="ts">
const slots = defineSlots<{
// 默认插槽
default(props: { message: string }): any
// 具名插槽
header(props: { title: string }): any
footer(): any
}>()
</script>
<template>
<div class="card">
<header v-if="slots.header">
<slot name="header" :title="title" />
</header>
<main>
<slot :message="message" />
</main>
<footer v-if="slots.footer">
<slot name="footer" />
</footer>
</div>
</template>
<!-- 使用 -->
<template>
<Card>
<template #header="{ title }">
<!-- title 有类型提示 -->
<h1>{{ title }}</h1>
</template>
<template #default="{ message }">
<!-- message 有类型提示 -->
<p>{{ message }}</p>
</template>
<template #footer>
<button>确定</button>
</template>
</Card>
</template>追问点:
- Q: defineSlots 是必须的吗?
- A: 不是,但推荐在 TypeScript 项目中使用
- Q: 如何检查插槽是否存在?
- A: 使用 slots.slotName 判断
4. Reactive Props 解构(Vue 3.5+)
难度: ⭐⭐⭐☆☆
问题: Vue 3.5 的响应式 Props 解构有什么特点?
答案:
响应式 Props 解构: 从 Vue 3.5 开始,解构 props 会保持响应性。
<script setup>
// ✅ Vue 3.5+ 解构保持响应性
const { count, message } = defineProps(['count', 'message'])
// 可以直接在 watch 中使用
watch(() => count, (newVal) => {
console.log('count changed:', newVal)
})
// 可以在 computed 中使用
const doubled = computed(() => count * 2)
</script>
<!-- ❌ Vue 3.4 及以前需要这样写 -->
<script setup>
const props = defineProps(['count', 'message'])
watch(() => props.count, (newVal) => {
console.log('count changed:', newVal)
})
</script>带默认值的解构:
<script setup>
// Vue 3.5+ 支持默认值
const { count = 0, message = 'Hello' } = defineProps(['count', 'message'])
</script>追问点:
- Q: 为什么 Vue 3.4 解构会失去响应性?
- A: 因为解构是值的拷贝,不是引用
- Q: Vue 3.5 如何实现响应式解构?
- A: 编译器会将解构转换为 getter 访问
5. 其他 Vue 3.4+ 改进
1. 更好的 Hydration 错误提示
<!-- Vue 3.4+ 会提供详细的 hydration 不匹配信息 -->
<script setup>
import { ref, onMounted } from 'vue'
const isClient = ref(false)
onMounted(() => {
isClient.value = true
})
</script>
<template>
<!-- ❌ 会导致 hydration 不匹配 -->
<div v-if="isClient">Client Only</div>
<!-- ✅ 正确做法 -->
<ClientOnly>
<div>Client Only</div>
</ClientOnly>
</template>2. defineOptions 宏(Vue 3.3+)
<script setup>
// 定义组件选项
defineOptions({
name: 'MyComponent',
inheritAttrs: false,
customOptions: {
// 自定义选项
}
})
</script>3. 更好的 TypeScript 支持
<script setup lang="ts">
// 更好的类型推断
const props = defineProps<{
count: number
message?: string
}>()
// 支持复杂类型
interface User {
id: number
name: string
}
const users = defineModel<User[]>()
</script>4. 性能改进
- 更快的响应式系统
- 更小的打包体积
- 更好的 Tree-shaking
追问点:
- Q: Vue 3.4 和 Vue 3.3 的主要区别?
- A: defineModel、更好的 TypeScript 支持、性能改进
- Q: 如何升级到 Vue 3.4+?
- A: 更新依赖,检查 breaking changes,测试应用
- Q: Vue 3.5 有哪些新特性?
- A: 响应式 Props 解构、更好的 SSR 支持、性能优化
反问环节
1. 团队的技术栈和开发规范?
问题:
- 使用的 Vue 版本?是否计划升级到 Vue 3?
- 状态管理方案?Pinia 还是 Vuex?
- UI 组件库?Element Plus、Ant Design Vue 还是自研?
- 是否使用 TypeScript?代码规范如何?
- 是否使用 Vite?构建工具是什么?
为什么问:
- 了解技术栈,评估学习成本
- 了解团队规范,快速融入
- 评估项目现代化程度
2. 项目架构和代码质量?
问题:
- 项目规模?代码行数?组件数量?
- 是否有组件库?Storybook?设计系统?
- 测试覆盖率?使用什么测试框架?
- CI/CD 流程?代码审查机制?
- 是否使用 Monorepo?如何管理多个项目?
为什么问:
- 评估项目复杂度
- 了解代码质量要求
- 了解工程化水平
3. 性能优化和监控?
问题:
- 是否有性能监控?使用什么工具?
- 首屏加载时间要求?
- 是否使用 SSR/SSG?
- 如何处理大数据量渲染?
- 移动端性能优化策略?
为什么问:
- 了解性能要求
- 评估技术挑战
- 了解优化经验
4. 团队协作和成长?
问题:
- 团队规模?前端团队多少人?
- 技术分享机制?
- 是否有导师制度?
- 技术选型的决策流程?
- 如何平衡业务需求和技术债?
为什么问:
- 了解团队氛围
- 评估成长空间
- 了解技术话语权
📚 学习资源
官方文档
- Vue 3 官方文档 - 最新的官方文档
- Vue Router 4 - 官方路由
- Pinia - 官方状态管理
- VueUse - Vue 组合式工具集
- Vite - 下一代构建工具
推荐阅读
- Vue.js 技术揭秘 - 深入理解 Vue 原理
- Vue.js 设计与实现 - HcySunYang 著
- Vue 3 源码解析 - 源码学习
社区资源
- Awesome Vue - Vue 资源大全
- Vue Land Discord - Vue 官方社区
- Vue.js Developers - Vue 开发者社区
🎯 总结
本文档涵盖了 Vue 3 面试的核心知识点:
基础题(必会):
- Composition API vs Options API
- ref vs reactive
- watch vs watchEffect
- 生命周期钩子
- 组件通信方式
进阶题(重要):
- Teleport 和 Suspense
- 性能优化最佳实践
- Composables 最佳实践
高级题(加分):
- Vue 3 响应式原理
- 编译优化原理
场景题(实战):
- 组件状态丢失问题
- 内存泄漏排查
- SSR 水合不匹配
掌握这些知识点,你将能够自信地应对 Vue 3 面试!
最后更新: 2025-02