主题
Vue
说明
共 15 题,难度 ⭐ ~ ⭐⭐⭐,覆盖 Vue 3 响应式原理、Composition API、虚拟 DOM、编译优化、组件通信等面试高频知识点。侧重与 React 对比,适合同时掌握两个框架的 5 年经验前端。
1. Vue 3 的响应式原理?Proxy 和 defineProperty 的区别? ⭐⭐⭐
对比 Vue 2 和 Vue 3 的响应式实现方式。
考察点:Proxy、依赖收集、派发更新
Vue 2:Object.defineProperty
javascript
function defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.depend() // 收集当前 watcher
}
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
dep.notify() // 通知所有 watcher
}
})
}
// 缺点:
// ❌ 无法检测属性的新增和删除 → 需要 Vue.set / Vue.delete
// ❌ 无法检测数组下标直接赋值 → arr[0] = 'x' 不触发更新
// ❌ 需要递归遍历所有属性,初始化性能差Vue 3:Proxy
typescript
function reactive<T extends object>(target: T): T {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key) // 依赖收集
const result = Reflect.get(target, key, receiver)
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
}
})
}对比
| 维度 | defineProperty (Vue 2) | Proxy (Vue 3) |
|---|---|---|
| 检测范围 | 已有属性的 get/set | 所有操作(get/set/delete/has/...) |
| 新增属性 | ❌ 需要 Vue.set | ✅ 自动检测 |
| 删除属性 | ❌ 需要 Vue.delete | ✅ 自动检测 |
| 数组操作 | ❌ 需要重写数组方法 | ✅ 原生支持 |
| 初始化 | 递归遍历所有属性(性能差) | 惰性代理(按需) |
| 兼容性 | IE9+ | 不支持 IE |
依赖收集与派发更新
依赖收集:
组件渲染 → 访问响应式数据 → 触发 get → track()
→ 将当前 effect(渲染函数)存入该属性的依赖集合
targetMap: WeakMap<object, Map<key, Set<effect>>>
{
obj: {
'name': [effect1, effect2],
'age': [effect3]
}
}
派发更新:
修改数据 → 触发 set → trigger()
→ 从 targetMap 中找到该属性的所有 effect → 重新执行追问延伸
ref和reactive的区别?什么时候用哪个?- 为什么 Proxy 要配合
Reflect使用?(保证 receiver 正确) - Vue 3 的
shallowRef/shallowReactive的使用场景?
2. ref 和 reactive 的区别?为什么有两套 API? ⭐⭐
对比两种响应式 API 的设计意图和使用场景。
考察点:响应式 API 选择
核心区别
| 维度 | ref | reactive |
|---|---|---|
| 接受类型 | 任意类型(原始值 + 对象) | 仅对象类型 |
| 访问方式 | .value | 直接访问 |
| 模板中 | 自动解包(不需要 .value) | 直接使用 |
| 解构 | 不丢失响应式 | ❌ 解构后丢失响应式 |
| 替换整体 | ref.value = newObj ✅ | ❌ 不能整体替换 |
| 底层实现 | 原始值用 getter/setter,对象内部用 reactive | Proxy |
为什么需要 ref
typescript
// Proxy 不能代理原始值
const count = reactive(0) // ❌ 不行!Proxy 只能代理 object
// ref 用 { value: T } 包装原始值
const count = ref(0) // ✅ 内部变成 { value: 0 }
count.value++ // 触发响应式解构问题
typescript
const state = reactive({ count: 0, name: 'Alice' })
// ❌ 解构后失去响应式
let { count, name } = state
count++ // 修改的是局部变量,不会触发更新
// ✅ 用 toRefs 转换
const { count, name } = toRefs(state)
count.value++ // 触发更新,因为 count 现在是 ref
// ✅ 或者从一开始就用 ref
const count = ref(0)
const name = ref('Alice')选择建议
用 ref:
- 原始值(number, string, boolean)
- 需要替换整个对象(如从 API 获取新数据)
- 需要在组合函数中返回(可解构)
- 模板 ref(DOM 引用)
用 reactive:
- 复杂对象且不需要解构
- 表单数据(多个字段组成的对象)
- 类似 React 的 useState 管理一组相关状态
实际项目中: ref 用得更多(更灵活,更安全)追问延伸
toRefs和toRef的区别?- 为什么在
<template>中不需要.value?(编译器自动处理) isRef()/isReactive()/isProxy()分别检查什么?
3. Vue 的 Diff 算法?双端对比是什么? ⭐⭐⭐
解释 Vue 3 的 Diff 算法和最长递增子序列优化。
考察点:虚拟 DOM、双端 Diff、LIS 优化
Vue 3 的 Diff 流程
Vue 3 使用的是"快速 Diff 算法",分 5 步:
第 1 步: 从头部开始比较,相同就 patch
第 2 步: 从尾部开始比较,相同就 patch
第 3 步: 头尾处理完后,新节点多出的 → 新增
第 4 步: 头尾处理完后,旧节点多出的 → 删除
第 5 步: 中间部分用 Map + 最长递增子序列(LIS)处理步骤详解
旧: a b [c d e f] g h
新: a b [e c d i] g h
第 1 步 (头部): a b 相同 → patch ✅
第 2 步 (尾部): g h 相同 → patch ✅
第 3-4 步: 中间部分不同 → 进入第 5 步
第 5 步 (中间乱序部分):
旧中间: c d e f
新中间: e c d i
① 建立新节点的 key → index 映射: { e:0, c:1, d:2, i:3 }
② 遍历旧中间节点,在映射中查找:
c → 在新中找到,位置 1
d → 在新中找到,位置 2
e → 在新中找到,位置 0
f → 在新中没找到 → 删除
③ 得到旧→新的位置映射: [1, 2, 0, -1]
LIS(最长递增子序列): [1, 2] → 对应 c, d 不需要移动
④ 最终操作:
c → 不移动(在 LIS 中)
d → 不移动(在 LIS 中)
e → 移动到 c 前面
i → 新增
f → 删除对比 React 和 Vue 的 Diff
| 维度 | React | Vue 3 |
|---|---|---|
| 算法 | 单向遍历 | 双端 + LIS |
| 移动策略 | lastPlacedIndex(单向) | 最长递增子序列(最优) |
| 效率 | 某些场景移动多余 | 移动操作最少 |
| 复杂度 | O(n) | O(n) + O(n log n) LIS |
经典例子: 把 [A B C] 变成 [C A B]
React (单向):
C(旧位置2 >= lastPlaced0) → 不动, lastPlaced=2
A(旧位置0 < lastPlaced2) → 移动 A
B(旧位置1 < lastPlaced2) → 移动 B
→ 移动 2 次
Vue (双端+LIS):
LIS = [A, B] (递增序列)
只需要移动 C → 1 次追问延伸
- LIS(最长递增子序列)的算法复杂度?Vue 用的什么实现?(贪心 + 二分 O(n log n))
- Vue 2 的双端 Diff 和 Vue 3 有什么区别?
- 为什么 Vue 不用 React 的 Fiber 架构?(Vue 的编译优化让 Diff 更快,不需要中断)
4. Composition API vs Options API 的设计差异? ⭐⭐
对比两种 API 风格的优劣和选择时机。
考察点:组合式 API
Options API 的问题
export default {
data() { return { name: '', age: 0, items: [] } },
computed: { ... },
methods: {
fetchUser() { ... }, // 用户相关
fetchItems() { ... }, // 列表相关
handleSort() { ... }, // 列表相关
},
watch: {
name() { ... }, // 用户相关
items() { ... }, // 列表相关
},
mounted() {
this.fetchUser() // 用户相关
this.fetchItems() // 列表相关
}
}
问题: 一个功能的代码分散在 data / methods / computed / watch / lifecycle 各处
→ "关注点分离"变成了"关注点碎片化"Composition API 的解决
typescript
// 按功能组织代码
function useUser() {
const name = ref('')
const age = ref(0)
async function fetchUser() { /* ... */ }
watch(name, () => { /* ... */ })
onMounted(() => fetchUser())
return { name, age, fetchUser }
}
function useItemList() {
const items = ref([])
async function fetchItems() { /* ... */ }
function handleSort() { /* ... */ }
watch(items, () => { /* ... */ })
onMounted(() => fetchItems())
return { items, fetchItems, handleSort }
}
// 组件中组合使用
export default {
setup() {
const { name, age } = useUser()
const { items, handleSort } = useItemList()
return { name, age, items, handleSort }
}
}对比
| 维度 | Options API | Composition API |
|---|---|---|
| 代码组织 | 按选项类型(data/methods/...) | 按功能逻辑 |
| 逻辑复用 | Mixins(有缺陷) | 组合函数(composables) |
| TypeScript | 类型推导弱 | ✅ 类型推导优秀 |
| 学习曲线 | 低(有结构约束) | 中(灵活度高) |
| 适用场景 | 小型组件、初学者 | 复杂组件、大型项目 |
类比 React Hooks
Vue Composition API ≈ React Hooks
ref() ≈ useState()
reactive() ≈ useState({...})
computed() ≈ useMemo()
watch() ≈ useEffect() + 依赖
onMounted() ≈ useEffect(() => {}, [])
composables ≈ custom hooks
关键区别:
React: 每次渲染重新执行组件函数(闭包)
Vue: setup() 只执行一次(响应式引用)
→ Vue 没有"闭包陷阱"、没有"依赖数组"的心智负担追问延伸
- Mixins 有哪些问题?Composition API 如何解决?
<script setup>语法糖的编译原理?- Vue 3 中 Options API 和 Composition API 能混用吗?(可以)
5. computed 和 watch 的区别?watchEffect 又是什么? ⭐⭐
对比 Vue 3 中三种响应式侦听方式。
考察点:响应式副作用
三者对比
| 维度 | computed | watch | watchEffect |
|---|---|---|---|
| 用途 | 派生状态 | 监听变化并执行副作用 | 自动追踪依赖的副作用 |
| 返回值 | ✅ 有(计算结果) | ❌ 无 | ❌ 无 |
| 依赖声明 | 自动追踪 | 显式指定 | 自动追踪 |
| 惰性 | ✅(用到才计算) | ✅(默认值变化才执行) | ❌(立即执行) |
| 缓存 | ✅(依赖不变不重算) | ❌ | ❌ |
computed
typescript
const firstName = ref('John')
const lastName = ref('Doe')
// 只读 computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// 可写 computed
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val: string) => {
const [first, last] = val.split(' ')
firstName.value = first
lastName.value = last
}
})watch
typescript
const userId = ref(1)
const userData = ref(null)
// 监听单个 ref
watch(userId, async (newId, oldId) => {
userData.value = await fetchUser(newId)
}, { immediate: true }) // immediate: 立即执行一次
// 监听多个源
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`${oldFirst} → ${newFirst}`)
})
// 监听 reactive 对象的某个属性(需要 getter)
const state = reactive({ count: 0 })
watch(() => state.count, (newCount) => {
console.log(newCount)
})
// deep watch(深层监听)
watch(state, (newState) => {
// reactive 对象默认 deep
}, { deep: true })watchEffect
typescript
// 自动追踪所有访问到的响应式数据
const stop = watchEffect((onCleanup) => {
const data = fetchData(userId.value) // 自动追踪 userId
onCleanup(() => {
// 清理副作用(类似 React useEffect 的 return)
})
})
// 手动停止
stop()选择指南
需要派生值(有返回值)?
→ computed ✅
需要知道旧值和新值?
→ watch ✅
需要副作用 + 自动追踪依赖?
→ watchEffect ✅
需要控制执行时机(flush: 'post' / 'sync')?
→ watch / watchEffect 都支持追问延伸
computed的缓存什么时候会失效?(依赖变化时 dirty 标记)watchPostEffect和watchSyncEffect是什么?- Vue 3.5 的
onWatcherCleanup和onCleanup有什么区别?
6. Vue 3 的编译优化?Block Tree 和 PatchFlags ⭐⭐⭐
解释 Vue 3 模板编译时的静态分析优化。
考察点:编译优化、静态提升
Vue 3 做了哪些编译优化
Vue 2: 运行时 Diff 遍历整棵虚拟 DOM 树
Vue 3: 编译时标记动态节点,运行时只 Diff 动态部分
优化手段:
① PatchFlags(补丁标记)
② Block Tree(块树)
③ 静态提升(Static Hoisting)
④ 事件监听缓存
⑤ SSR 优化PatchFlags
vue
<template>
<div>
<p>静态文本</p> <!-- 无标记 -->
<p>{{ message }}</p> <!-- TEXT = 1 -->
<p :class="cls">文本</p> <!-- CLASS = 2 -->
<p :id="id" :class="cls">文本</p> <!-- PROPS = 8 -->
</div>
</template>javascript
// 编译后的渲染函数
function render() {
return createBlock('div', null, [
createVNode('p', null, '静态文本'),
createVNode('p', null, ctx.message, 1 /* TEXT */),
createVNode('p', { class: ctx.cls }, '文本', 2 /* CLASS */),
createVNode('p', { id: ctx.id, class: ctx.cls }, '文本', 8 /* PROPS */, ['id', 'class']),
])
}
// Diff 时根据 PatchFlags 只比较必要部分:
// TEXT → 只比较 textContent
// CLASS → 只比较 class
// PROPS → 只比较指定的 props (id, class)Block Tree
普通 Diff: 递归遍历整棵树
Block Tree: 收集动态节点到 dynamicChildren 数组,扁平化遍历
<div> Block Root
<p>static</p> 跳过
<p>static</p> 跳过
<p>{{ msg }}</p> → dynamicChildren[0]
<div>
<span>static</span> 跳过
<span>{{ a }}</span> → dynamicChildren[1]
</div>
</div>
Diff 时: 只遍历 dynamicChildren = [<p>{{ msg }}</p>, <span>{{ a }}</span>]
跳过所有静态节点!静态提升
javascript
// 未优化: 每次渲染都创建
function render() {
return createVNode('div', null, [
createVNode('p', null, '静态文本'), // 每次 render 都创建新 VNode
createVNode('p', null, ctx.message),
])
}
// 静态提升后: 静态 VNode 只创建一次
const _hoisted = createVNode('p', null, '静态文本')
function render() {
return createVNode('div', null, [
_hoisted, // 复用同一个 VNode 对象
createVNode('p', null, ctx.message),
])
}事件监听缓存
javascript
// 未缓存: 每次渲染创建新函数 → 子组件被迫更新
createVNode('button', { onClick: () => ctx.handleClick() })
// 缓存后: 复用同一个函数引用
createVNode('button', {
onClick: cache[0] || (cache[0] = () => ctx.handleClick())
})
// 类似 React 的 useCallback,但 Vue 编译器自动处理追问延伸
- 为什么 React 不能做类似的编译优化?(JSX 太灵活,无法静态分析)
- Vue Vapor Mode 是什么?它不使用虚拟 DOM?
- 可以在 Vue Playground 中查看编译输出吗?(template-explorer.vuejs.org)
7. Vue 的组件通信有哪些方式? ⭐
全面梳理 Vue 3 中组件之间的数据传递方式。
考察点:组件通信
通信方式汇总
| 方式 | 方向 | 适用关系 |
|---|---|---|
props | 父 → 子 | 直接父子 |
emit | 子 → 父 | 直接父子 |
v-model | 父 ↔ 子 | 直接父子(双向) |
provide / inject | 祖先 → 后代 | 跨层级 |
expose / ref | 父 → 子(命令式) | 直接父子 |
attrs / slots | 父 → 子 | 透传 |
| Event Bus | 任意 → 任意 | 兄弟/跨层级(不推荐) |
| Pinia | 全局 | 全局共享 |
核心方式示例
vue
<!-- 1. props + emit -->
<!-- Parent.vue -->
<Child :name="name" @update="handleUpdate" />
<!-- Child.vue -->
<script setup>
const props = defineProps<{ name: string }>()
const emit = defineEmits<{ update: [value: string] }>()
</script>
<!-- 2. v-model(语法糖) -->
<!-- Parent.vue -->
<Child v-model:title="title" v-model:content="content" />
<!-- Child.vue -->
<script setup>
const props = defineProps<{ title: string; content: string }>()
const emit = defineEmits<{
'update:title': [value: string]
'update:content': [value: string]
}>()
</script>
<!-- 3. provide / inject -->
<!-- Ancestor.vue -->
<script setup>
const theme = ref('dark')
provide('theme', theme) // 提供响应式 ref
</script>
<!-- DeepChild.vue -->
<script setup>
const theme = inject('theme') // 注入
</script>
<!-- 4. expose + ref -->
<!-- Parent.vue -->
<script setup>
const childRef = ref()
childRef.value?.doSomething() // 调用子组件暴露的方法
</script>
<template>
<Child ref="childRef" />
</template>
<!-- Child.vue -->
<script setup>
function doSomething() { /* ... */ }
defineExpose({ doSomething })
</script>追问延伸
defineModel()(Vue 3.4+)如何简化v-model的实现?provide/inject和 React Context 有什么异同?- 为什么不推荐 Event Bus?Pinia 如何替代?
8. Vue Router 的导航守卫?路由懒加载? ⭐⭐
说明 Vue Router 的守卫机制和代码分割。
考察点:路由
导航守卫的完整执行顺序
导航从 /a 到 /b:
1. 组件 A 的 beforeRouteLeave (组件内守卫)
2. router.beforeEach (全局前置守卫)
3. 组件 B 的 beforeRouteEnter (组件内守卫,组件还没创建)
/ 复用组件的 beforeRouteUpdate
4. 路由配置的 beforeEnter (路由独享守卫)
5. 解析异步路由组件
6. router.beforeResolve (全局解析守卫)
7. 导航确认
8. router.afterEach (全局后置守卫)
9. DOM 更新
10. 组件 B 的 beforeRouteEnter 的 next 回调实际用法
typescript
const router = createRouter({
routes: [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'), // 路由懒加载
meta: { requiresAuth: true },
beforeEnter: (to, from) => {
// 路由独享守卫
}
}
]
})
// 全局前置守卫 — 权限控制
router.beforeEach((to, from) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
return { name: 'login', query: { redirect: to.fullPath } }
}
})
// Composition API 中使用
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
return window.confirm('有未保存的修改,确定离开?')
}
})路由懒加载 + 代码分割
typescript
// 自动按路由分割代码
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'),
// Vite 会自动将 Dashboard.vue 打包为独立 chunk
},
{
path: '/settings',
component: () => import('./views/Settings.vue'),
}
]
// 命名 chunk(Webpack / Vite)
component: () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue')追问延伸
router.beforeResolve和router.beforeEach有什么区别?- 如何实现路由级别的
<KeepAlive>缓存? - Vue Router 的
scrollBehavior如何控制滚动位置?
9. nextTick 的原理?什么时候需要用? ⭐⭐
解释 Vue 的异步更新机制和 nextTick 的实现。
考察点:异步更新
Vue 的异步更新机制
修改数据 → 不立即更新 DOM → 将更新放入队列
→ 在同一事件循环中的所有修改合并
→ 微任务中统一执行 DOM 更新
state.count = 1
state.count = 2
state.count = 3
// 只触发一次 DOM 更新,最终 count = 3nextTick 的作用
typescript
import { ref, nextTick } from 'vue'
const count = ref(0)
const divRef = ref<HTMLDivElement>()
async function increment() {
count.value++
// ❌ DOM 还没更新
console.log(divRef.value?.textContent) // '0'(旧值)
// ✅ 等待 DOM 更新后
await nextTick()
console.log(divRef.value?.textContent) // '1'(新值)
}nextTick 的实现
typescript
// Vue 3 的 nextTick 基于 Promise.resolve()(微任务)
const resolvedPromise = Promise.resolve()
let currentFlushPromise = null
function nextTick(fn) {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(fn) : p
}
// 异步更新的调度:
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
// flushJobs 执行所有排队的组件更新
}
}使用场景
typescript
// ① 修改数据后需要立即操作 DOM
const list = ref([])
async function addItem() {
list.value.push({ id: Date.now() })
await nextTick()
scrollToBottom() // 滚动到新添加的项
}
// ② 在 setup / onMounted 中需要确保 DOM 就绪
onMounted(async () => {
await nextTick()
initThirdPartyLibrary(containerRef.value)
})
// ③ 获取更新后的组件尺寸
async function expand() {
isExpanded.value = true
await nextTick()
const height = contentRef.value?.offsetHeight // 展开后的高度
}追问延伸
- Vue 2 的 nextTick 和 Vue 3 有什么区别?(Vue 2 有降级策略: Promise → MutationObserver → setImmediate → setTimeout)
nextTick和 React 的flushSync解决的是同一个问题吗?- 多次调用
nextTick的执行顺序?(按调用顺序,都在同一个微任务中)
10. Pinia 和 Vuex 的核心区别? ⭐⭐
对比 Vue 官方推荐的两代状态管理方案。
考察点:状态管理
核心区别
| 维度 | Vuex 4 | Pinia |
|---|---|---|
| API 风格 | Options(mutations/actions) | Composition / Options 可选 |
| Mutations | ✅ 必须通过 mutation 修改 | ❌ 移除,直接修改 state |
| 模块化 | modules + namespaced | 独立 Store,自动命名空间 |
| TypeScript | 类型推导差 | ✅ 类型推导完美 |
| 体积 | ~1KB(不含 devtools) | ~1KB |
| Devtools | ✅ | ✅ |
| SSR | 需要额外配置 | 内置支持 |
Pinia 的基本用法
typescript
// stores/counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// Composition API 风格(推荐)
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
async function fetchCount() {
const res = await fetch('/api/count')
count.value = await res.json()
}
return { count, doubleCount, increment, fetchCount }
})
// Options API 风格
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() { this.count++ },
async fetchCount() {
this.count = await fetch('/api/count').then(r => r.json())
},
},
})vue
<!-- 组件中使用 -->
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// ❌ 直接解构会丢失响应式
const { count } = store
// ✅ 用 storeToRefs 保持响应式
const { count, doubleCount } = storeToRefs(store)
// actions 可以直接解构
const { increment } = store
</script>为什么 Pinia 移除了 Mutations
Vuex:
state → 只能通过 mutation 同步修改
mutation 的意义: Devtools 追踪每次变更
Pinia:
state → 可以直接修改
→ Pinia 内部通过 $patch 和 $subscribe 实现了追踪
→ Devtools 同样能追踪每次变更
→ 少了一层 mutation 的样板代码Store 间互相调用
typescript
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const authStore = useAuthStore() // 直接 import 另一个 store
const name = ref('')
function logout() {
name.value = ''
authStore.clearToken() // 调用其他 store 的 action
}
return { name, logout }
})追问延伸
- Pinia 的
$subscribe和 Vuex 的subscribe有什么区别? - Pinia 插件系统怎么用?(持久化、日志等)
- 对比 Pinia 和 Zustand 的 API 设计?
11. <KeepAlive> 的原理?LRU 缓存策略? ⭐⭐
解释 KeepAlive 组件的缓存机制和生命周期。
考察点:组件缓存、LRU
KeepAlive 做了什么
正常的组件切换:
显示 A → 切到 B → A 被销毁(unmounted)→ 切回 A → A 重新创建
KeepAlive 包裹的组件:
显示 A → 切到 B → A 被"冻结"(deactivated)→ 切回 A → A 被"激活"(activated)
→ A 的 DOM 和状态完整保留基本用法
vue
<template>
<!-- 基本用法 -->
<KeepAlive>
<component :is="currentTab" />
</KeepAlive>
<!-- include / exclude(字符串、正则、数组) -->
<KeepAlive :include="['TabA', 'TabB']" :exclude="/TabC/">
<component :is="currentTab" />
</KeepAlive>
<!-- max — 最多缓存 N 个组件(LRU 策略) -->
<KeepAlive :max="5">
<component :is="currentTab" />
</KeepAlive>
<!-- 配合 Vue Router -->
<RouterView v-slot="{ Component }">
<KeepAlive :include="cachedViews">
<component :is="Component" />
</KeepAlive>
</RouterView>
</template>生命周期
typescript
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 组件从缓存中被激活(进入页面)
// 适合: 刷新数据、恢复定时器、恢复滚动位置
fetchLatestData()
})
onDeactivated(() => {
// 组件被缓存(离开页面)
// 适合: 暂停定时器、取消请求
clearInterval(timer)
})LRU 缓存原理
KeepAlive 内部维护一个缓存 Map + keys Set:
cache: Map<key, VNode>
keys: Set<key>
当 max = 3 时:
访问 A → cache: [A], keys: {A}
访问 B → cache: [A, B], keys: {A, B}
访问 C → cache: [A, B, C], keys: {A, B, C}
访问 D → 超过 max!LRU 淘汰最久未使用的 A
cache: [B, C, D], keys: {B, C, D}
再访问 B → B 移到"最近使用"
keys: {C, D, B}(B 变为最新)追问延伸
<KeepAlive>和 React 的 Offscreen(Activity)有什么异同?- 如何手动清除
<KeepAlive>的缓存?(动态修改 include/exclude) - KeepAlive 组件缓存的 DOM 保存在哪里?(移入
document.createDocumentFragment)
12. <Teleport> 和 <Suspense> 的使用场景? ⭐⭐
说明 Vue 3 新增的两个内置组件。
考察点:内置组件
<Teleport> — 传送门
vue
<!-- 问题: Modal 嵌套在组件树深处,z-index 和 overflow 受限 -->
<div class="parent" style="overflow: hidden;">
<Modal /> <!-- 被父容器裁剪 -->
</div>
<!-- Teleport: 将 DOM 传送到指定位置 -->
<template>
<button @click="showModal = true">打开</button>
<Teleport to="body">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<p>弹窗内容</p>
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
</template>
<!-- 逻辑上属于当前组件(响应式/事件正常工作) -->
<!-- DOM 上渲染到 body 下(脱离父容器限制) -->vue
<!-- 常见使用场景 -->
<!-- 1. Modal / Dialog -->
<Teleport to="body"><Modal /></Teleport>
<!-- 2. Toast / Notification -->
<Teleport to="#toast-container"><Toast /></Teleport>
<!-- 3. Tooltip / Popover -->
<Teleport to="body"><Tooltip /></Teleport>
<!-- 4. 条件禁用 -->
<Teleport to="body" :disabled="isMobile">
<Sidebar />
</Teleport><Suspense> — 异步组件加载
vue
<template>
<Suspense>
<!-- 默认插槽: 异步组件 -->
<template #default>
<AsyncDashboard />
</template>
<!-- fallback 插槽: 加载中显示 -->
<template #fallback>
<LoadingSkeleton />
</template>
</Suspense>
</template>
<script setup>
// AsyncDashboard.vue — 异步 setup
const data = await fetch('/api/dashboard').then(r => r.json())
// setup 中有 await → 该组件变成异步组件
// Suspense 会等待它 resolve 后再显示
</script>嵌套 Suspense
vue
<Suspense>
<template #default>
<div>
<UserProfile /> <!-- 快速加载 -->
<Suspense>
<template #default>
<HeavyChart /> <!-- 慢数据 -->
</template>
<template #fallback>
<ChartSkeleton />
</template>
</Suspense>
</div>
</template>
<template #fallback>
<FullPageLoader />
</template>
</Suspense>对比 React 的同名组件
| 维度 | Vue <Suspense> | React <Suspense> |
|---|---|---|
| 触发方式 | async setup() | throw Promise / React.lazy |
| 状态 | 实验性(Experimental) | 稳定 |
| 数据获取 | async setup 直接 await | 需要配合 use() 或库 |
追问延伸
<Teleport>和 React 的createPortal有什么区别?<Suspense>的onPending/onResolve/onFallback事件有什么用?- 多个
<Teleport>可以传送到同一个目标吗?(可以,按顺序追加)
13. Vue 和 React 的核心设计差异? ⭐⭐⭐
从架构设计层面对比两个框架。
考察点:框架设计、编译时 vs 运行时
核心理念差异
| 维度 | Vue | React |
|---|---|---|
| 定位 | 渐进式框架 | UI 库 |
| 响应式 | Proxy 细粒度追踪 | 不可变数据 + 重渲染 |
| 模板 | 编译型模板(SFC) | JSX(运行时) |
| 更新粒度 | 组件级精确更新 | 组件树自顶向下 diff |
| 优化策略 | 编译时优化(PatchFlags) | 运行时优化(memo/Fiber) |
| 心智模型 | 可变数据 + 自动依赖追踪 | 不可变数据 + 显式声明依赖 |
更新机制对比
Vue:
state.count = 1
→ Proxy 拦截 set
→ 找到依赖这个属性的组件
→ 精确更新这些组件
→ 不需要 shouldComponentUpdate / memo
React:
setState(1)
→ 标记组件 dirty
→ 从该组件开始向下 diff 整棵子树
→ 所有子组件默认重渲染
→ 需要 React.memo / useMemo 手动优化编译时 vs 运行时
Vue(编译时优化):
模板 → 编译器 → 优化后的渲染函数
✅ PatchFlags、Block Tree、静态提升
✅ 运行时 Diff 更少的工作
❌ 模板表达力受限
React(运行时优化):
JSX → createElement 调用 → 完整 Diff
✅ JSX 完全就是 JavaScript,极其灵活
❌ 需要 Fiber 架构处理大组件树
❌ 需要 React Compiler 弥补编译优化状态管理思维
javascript
// Vue: 可变数据,直接修改
const state = reactive({ items: [] })
state.items.push(newItem) // ✅ 直接修改,Vue 自动检测
// React: 不可变数据,创建新引用
const [items, setItems] = useState([])
setItems([...items, newItem]) // ✅ 创建新数组
// items.push(newItem) // ❌ 不会触发更新如何选择
选 Vue:
- 团队对 Vue 更熟悉
- 渐进式引入(CDN script 标签也能用)
- 更低的心智负担(不用操心 memo/依赖数组)
- 内置功能丰富(transition, keep-alive, v-model...)
选 React:
- 生态更大(社区、第三方库、招聘市场)
- JSX 的灵活性(复杂 UI 逻辑)
- React Native 跨平台需求
- Next.js / RSC 的前沿技术
- 函数式编程风格偏好追问延伸
- Vue Vapor Mode 和 Svelte 的"无虚拟 DOM"有什么区别?
- 为什么 React 需要 Fiber 而 Vue 不需要?(Vue 编译时优化 + 精确更新 → Diff 更快)
- 两个框架的 SSR 方案(Nuxt vs Next)的核心差异?
14. Vue 3 的 <script setup> 编译做了什么? ⭐⭐
解析
<script setup>语法糖的编译原理。
考察点:编译原理、DX
<script setup> 前后对比
vue
<!-- 不用 <script setup> -->
<script>
import { ref } from 'vue'
import MyChild from './MyChild.vue'
export default {
components: { MyChild },
props: { title: String },
emits: ['update'],
setup(props, { emit }) {
const count = ref(0)
function increment() { count.value++ }
return { count, increment }
}
}
</script>
<!-- 使用 <script setup> -->
<script setup>
import { ref } from 'vue'
import MyChild from './MyChild.vue'
const props = defineProps<{ title: string }>()
const emit = defineEmits<{ update: [value: string] }>()
const count = ref(0)
function increment() { count.value++ }
// 不需要 return,所有顶层绑定自动暴露给模板
// 不需要 components,import 的组件自动注册
</script>编译器做了什么
javascript
// <script setup> 编译后的实际代码(简化)
import { ref, defineComponent } from 'vue'
import MyChild from './MyChild.vue'
export default defineComponent({
props: { title: { type: String } },
emits: ['update'],
setup(__props, { expose, emit }) {
const count = ref(0)
function increment() { count.value++ }
// 编译器自动收集所有顶层变量
return {
count,
increment,
MyChild, // 组件也被收集
}
}
})编译器宏(Compiler Macros)
typescript
// 这些函数不需要 import,编译时会被替换
defineProps<{ title: string }>() // → 编译为 props 选项
defineEmits<{ click: [e: Event] }>() // → 编译为 emits 选项
defineExpose({ method }) // → 编译为 expose 调用
defineModel() // → 编译为 prop + emit (3.4+)
defineOptions({ inheritAttrs: false }) // → 编译为组件选项
defineSlots<{ default: { msg: string } }>() // → 类型声明
withDefaults(defineProps<Props>(), { // → 默认值
title: 'hello'
})ref 在模板中的自动解包
vue
<script setup>
const count = ref(0)
</script>
<template>
<!-- 模板中不需要 .value -->
<p>{{ count }}</p>
<!-- 编译为: _ctx.count.value(编译器自动添加 .value) -->
</template>追问延伸
defineProps为什么不需要 import?它在运行时存在吗?(纯编译时宏,编译后被移除)<script setup>和普通<script>可以共存吗?(可以,用于inheritAttrs等场景)- Vue Macros 插件扩展了哪些能力?
15. Vue 3 性能优化的手段有哪些? ⭐⭐
给出 Vue 3 项目性能优化的完整清单。
考察点:性能优化
渲染优化
vue
<!-- ① v-once — 只渲染一次 -->
<h1 v-once>{{ title }}</h1>
<!-- ② v-memo — 有条件的缓存(Vue 3.2+) -->
<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
<!-- 只有 item.selected 变化时才重渲染这个项 -->
<HeavyComponent :data="item" />
</div>
<!-- ③ 合理使用 KeepAlive -->
<KeepAlive :max="10" :include="cachedViews">
<component :is="currentView" />
</KeepAlive>
<!-- ④ 列表渲染使用 key -->
<div v-for="item in items" :key="item.id">{{ item.name }}</div>响应式优化
typescript
// ① shallowRef / shallowReactive — 大数据只追踪顶层
const bigList = shallowRef<Item[]>([])
// 修改内部不触发更新,需要整体替换
bigList.value = [...bigList.value, newItem]
// ② markRaw — 标记为非响应式
import { markRaw } from 'vue'
const heavyLib = markRaw(new EChartsInstance())
// ECharts 实例不需要响应式追踪,避免性能开销
// ③ computed 缓存
const filtered = computed(() =>
items.value.filter(i => i.active)
)
// 只有 items 变化才重新计算,模板多次引用不重复计算组件优化
typescript
// ① 异步组件 — 按需加载
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: LoadingSkeleton,
delay: 200,
errorComponent: ErrorDisplay,
timeout: 3000,
})
// ② 函数式组件(无状态的轻量组件)
function RenderItem(props: { item: Item }) {
return h('div', props.item.name)
}构建优化
typescript
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-lib': ['element-plus'],
}
}
}
}
})优化清单汇总
| 分类 | 手段 | 效果 |
|---|---|---|
| 渲染 | v-once / v-memo | 减少不必要的重渲染 |
| 渲染 | <KeepAlive> | 避免频繁销毁/创建 |
| 渲染 | 虚拟列表 | 大列表只渲染可见区域 |
| 响应式 | shallowRef / markRaw | 减少 Proxy 代理深度 |
| 响应式 | computed 缓存 | 避免重复计算 |
| 加载 | 异步组件 / 路由懒加载 | 减少首屏 JS 体积 |
| 加载 | Tree-shaking | Vue 3 全局 API 可 tree-shake |
| 构建 | 代码分割 / manualChunks | 并行下载、缓存利用 |
| 运行时 | v-show vs v-if | 频繁切换用 v-show |
追问延伸
v-memo和 React 的React.memo有什么区别?- Vue 3 的 Tree-shaking 是如何实现的?(全局 API 改为具名导出)
markRaw和Object.freeze有什么区别?