2103 lines
46 KiB
Markdown
2103 lines
46 KiB
Markdown
# Vue 3 深度面试题及解析
|
||
|
||
> 本文档涵盖 Vue 3 核心原理、Composition API、响应式系统、性能优化等高频面试考点,适合中高级前端工程师面试准备。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [响应式系统原理](#1-响应式系统原理)
|
||
2. [Composition API 深度](#2-composition-api-深度)
|
||
3. [虚拟 DOM 与 Diff 算法](#3-虚拟-dom-与-diff-算法)
|
||
4. [编译优化](#4-编译优化)
|
||
5. [生命周期与调度机制](#5-生命周期与调度机制)
|
||
6. [组件通信与状态管理](#6-组件通信与状态管理)
|
||
7. [性能优化](#7-性能优化)
|
||
8. [Vapor Mode 深度解析](#8-vapor-mode-深度解析)
|
||
9. [Vue 3 与 Vue 2 核心区别](#9-vue-3-与-vue-2-核心区别)
|
||
10. [实战场景题](#10-实战场景题)
|
||
|
||
---
|
||
|
||
## 1. 响应式系统原理
|
||
|
||
### Q1: Vue 3 的响应式原理是什么?与 Vue 2 有什么本质区别?
|
||
|
||
**答案:**
|
||
|
||
**Vue 3 响应式原理(基于 Proxy):**
|
||
|
||
```javascript
|
||
// Vue 3 响应式核心简化实现
|
||
function reactive(target) {
|
||
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
|
||
}
|
||
})
|
||
}
|
||
```
|
||
|
||
**核心区别对比:**
|
||
|
||
| 特性 | Vue 2 (Object.defineProperty) | Vue 3 (Proxy) |
|
||
|------|------------------------------|---------------|
|
||
| 数组监听 | 需要重写数组方法 | 原生支持 |
|
||
| 新增/删除属性 | 需要 `Vue.set/Vue.delete` | 自动检测 |
|
||
| 性能 | 递归遍历所有属性 | 惰性代理(访问时才代理) |
|
||
| Map/Set/WeakMap | 不支持 | 原生支持 |
|
||
| 深层嵌套 | 初始化时递归 | 访问时才递归(懒代理) |
|
||
|
||
**深度解析:**
|
||
|
||
1. **惰性代理(Lazy Proxy)**:Vue 3 只有在访问嵌套对象时才会创建其代理,大幅提升初始化性能
|
||
2. **依赖收集更精准**:通过 `WeakMap -> Map -> Set` 的数据结构存储依赖关系
|
||
3. **支持更多数据类型**:Proxy 可以拦截更多操作(如 `in`、`delete` 等)
|
||
|
||
---
|
||
|
||
### Q2: 请详细解释 `ref` 和 `reactive` 的区别及实现原理
|
||
|
||
**答案:**
|
||
|
||
```javascript
|
||
// ref 核心实现
|
||
function ref(value) {
|
||
return new RefImpl(value)
|
||
}
|
||
|
||
class RefImpl {
|
||
private _value
|
||
public dep = new Set()
|
||
public __v_isRef = true
|
||
|
||
constructor(value) {
|
||
this._value = isObject(value) ? reactive(value) : value
|
||
}
|
||
|
||
get value() {
|
||
trackRefValue(this)
|
||
return this._value
|
||
}
|
||
|
||
set value(newVal) {
|
||
if (hasChanged(newVal, this._value)) {
|
||
this._value = isObject(newVal) ? reactive(newVal) : newVal
|
||
triggerRefValue(this)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**核心区别:**
|
||
|
||
| 特性 | ref | reactive |
|
||
|------|-----|----------|
|
||
| 适用类型 | 任意类型(基本类型推荐) | 仅对象类型 |
|
||
| 访问方式 | 需要 `.value` | 直接访问 |
|
||
| 解构 | 保持响应式 | 失去响应式(需 toRefs) |
|
||
| 模板中使用 | 自动解包(无需 .value) | 直接使用 |
|
||
| 重新赋值 | 保持响应式 | 失去响应式 |
|
||
|
||
**面试加分点:**
|
||
- `ref` 使用 class 的 getter/setter 实现,而非 Proxy
|
||
- `shallowRef` 只追踪 `.value` 的变化,不做深层响应式
|
||
- `triggerRef` 可以强制触发 shallowRef 的更新
|
||
|
||
---
|
||
|
||
### Q3: 请解释 Vue 3 的依赖收集和触发更新的完整流程
|
||
|
||
**答案:**
|
||
|
||
```javascript
|
||
// 依赖收集核心数据结构
|
||
// targetMap: WeakMap<target, Map<key, Set<effect>>>
|
||
const targetMap = new WeakMap()
|
||
|
||
// 当前正在执行的 effect
|
||
let activeEffect = null
|
||
const effectStack = []
|
||
|
||
function effect(fn, options = {}) {
|
||
const effectFn = () => {
|
||
cleanup(effectFn) // 清除旧依赖
|
||
activeEffect = effectFn
|
||
effectStack.push(effectFn)
|
||
const result = fn()
|
||
effectStack.pop()
|
||
activeEffect = effectStack[effectStack.length - 1]
|
||
return result
|
||
}
|
||
effectFn.deps = []
|
||
effectFn.options = options
|
||
if (!options.lazy) {
|
||
effectFn()
|
||
}
|
||
return effectFn
|
||
}
|
||
|
||
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()))
|
||
}
|
||
if (!dep.has(activeEffect)) {
|
||
dep.add(activeEffect)
|
||
activeEffect.deps.push(dep) // 反向收集,用于清理
|
||
}
|
||
}
|
||
|
||
function trigger(target, key) {
|
||
const depsMap = targetMap.get(target)
|
||
if (!depsMap) return
|
||
const effects = depsMap.get(key)
|
||
const effectsToRun = new Set()
|
||
effects && effects.forEach(effect => {
|
||
// 避免无限递归
|
||
if (effect !== activeEffect) {
|
||
effectsToRun.add(effect)
|
||
}
|
||
})
|
||
effectsToRun.forEach(effect => {
|
||
if (effect.options.scheduler) {
|
||
effect.options.scheduler(effect)
|
||
} else {
|
||
effect()
|
||
}
|
||
})
|
||
}
|
||
```
|
||
|
||
**流程图解:**
|
||
|
||
```
|
||
组件渲染 → 创建 effect → 执行 render 函数 → 访问响应式数据
|
||
↓ ↓
|
||
模板更新 ← scheduler 调度 ← trigger ← 数据变化 track(收集依赖)
|
||
```
|
||
|
||
---
|
||
|
||
### Q4: 什么是 `effectScope`?它解决了什么问题?
|
||
|
||
**答案:**
|
||
|
||
`effectScope` 是 Vue 3.2+ 引入的 API,用于批量管理和清理副作用。
|
||
|
||
```javascript
|
||
import { effectScope, ref, watch, computed } from 'vue'
|
||
|
||
const scope = effectScope()
|
||
|
||
scope.run(() => {
|
||
const count = ref(0)
|
||
|
||
// 这些副作用都会被 scope 收集
|
||
const doubled = computed(() => count.value * 2)
|
||
|
||
watch(count, () => {
|
||
console.log('count changed')
|
||
})
|
||
})
|
||
|
||
// 一次性清理所有副作用
|
||
scope.stop()
|
||
```
|
||
|
||
**解决的问题:**
|
||
|
||
1. **组合式函数中的副作用管理**:不需要手动收集和清理每个 watch/computed
|
||
2. **内存泄漏预防**:确保组件卸载时所有副作用都被清理
|
||
3. **跨组件共享状态的副作用管理**:如 Pinia 内部就使用了 effectScope
|
||
|
||
**面试加分点:**
|
||
- `getCurrentScope()` 获取当前活跃的 scope
|
||
- `onScopeDispose()` 注册 scope 销毁时的回调
|
||
- 组件的 `setup()` 函数内部就运行在一个 effectScope 中
|
||
|
||
---
|
||
|
||
## 2. Composition API 深度
|
||
|
||
### Q5: `setup` 函数的执行时机和上下文是什么?
|
||
|
||
**答案:**
|
||
|
||
```javascript
|
||
export default {
|
||
props: ['initialCount'],
|
||
setup(props, context) {
|
||
// props: 响应式的 props 对象(使用 toRefs 解构)
|
||
// context: { attrs, slots, emit, expose }
|
||
|
||
console.log('setup 执行')
|
||
|
||
onBeforeMount(() => console.log('beforeMount'))
|
||
onMounted(() => console.log('mounted'))
|
||
|
||
return { /* 暴露给模板的数据 */ }
|
||
},
|
||
beforeCreate() {
|
||
console.log('beforeCreate')
|
||
},
|
||
created() {
|
||
console.log('created')
|
||
}
|
||
}
|
||
|
||
// 执行顺序:setup → beforeCreate → created → beforeMount → mounted
|
||
```
|
||
|
||
**关键点:**
|
||
|
||
1. **执行时机**:在 `beforeCreate` 之前执行
|
||
2. **没有 `this`**:setup 中不能访问 this
|
||
3. **只执行一次**:setup 只在组件初始化时执行一次
|
||
4. **context 不是响应式的**:可以直接解构
|
||
|
||
**props 注意事项:**
|
||
```javascript
|
||
setup(props) {
|
||
// ❌ 错误:解构会失去响应式
|
||
const { count } = props
|
||
|
||
// ✅ 正确:使用 toRefs
|
||
const { count } = toRefs(props)
|
||
|
||
// ✅ 或使用 toRef
|
||
const count = toRef(props, 'count')
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Q6: `watch` 和 `watchEffect` 的区别及使用场景?
|
||
|
||
**答案:**
|
||
|
||
```javascript
|
||
import { ref, watch, watchEffect } from 'vue'
|
||
|
||
const count = ref(0)
|
||
const name = ref('Vue')
|
||
|
||
// watchEffect: 自动收集依赖,立即执行
|
||
watchEffect(() => {
|
||
console.log(`count: ${count.value}, name: ${name.value}`)
|
||
})
|
||
|
||
// watch: 明确指定依赖,惰性执行
|
||
watch(count, (newVal, oldVal) => {
|
||
console.log(`count: ${oldVal} → ${newVal}`)
|
||
})
|
||
|
||
// watch 多个源
|
||
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
|
||
// ...
|
||
})
|
||
|
||
// watch 深层对象
|
||
const state = reactive({ nested: { count: 0 } })
|
||
watch(
|
||
() => state.nested.count,
|
||
(count) => console.log(count)
|
||
)
|
||
```
|
||
|
||
**核心区别:**
|
||
|
||
| 特性 | watch | watchEffect |
|
||
|------|-------|-------------|
|
||
| 依赖收集 | 显式指定 | 自动收集 |
|
||
| 执行时机 | 惰性(默认) | 立即执行 |
|
||
| 获取旧值 | ✅ 可以 | ❌ 不可以 |
|
||
| 深度监听 | 需要配置 deep | 自动深度追踪 |
|
||
| 使用场景 | 需要旧值/条件执行 | 副作用同步 |
|
||
|
||
**高级用法:**
|
||
|
||
```javascript
|
||
// 清理副作用
|
||
watchEffect((onCleanup) => {
|
||
const controller = new AbortController()
|
||
fetch(url, { signal: controller.signal })
|
||
|
||
onCleanup(() => {
|
||
controller.abort()
|
||
})
|
||
})
|
||
|
||
// flush 时机控制
|
||
watchEffect(callback, {
|
||
flush: 'post' // 'pre' | 'post' | 'sync'
|
||
})
|
||
|
||
// watchPostEffect 等价于 flush: 'post'
|
||
watchPostEffect(() => {
|
||
// DOM 更新后执行
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
### Q7: 请解释 `computed` 的缓存机制和实现原理
|
||
|
||
**答案:**
|
||
|
||
```javascript
|
||
// computed 简化实现
|
||
function computed(getterOrOptions) {
|
||
let getter, setter
|
||
|
||
if (typeof getterOrOptions === 'function') {
|
||
getter = getterOrOptions
|
||
setter = () => console.warn('computed is readonly')
|
||
} else {
|
||
getter = getterOrOptions.get
|
||
setter = getterOrOptions.set
|
||
}
|
||
|
||
let value
|
||
let dirty = true // 脏值标记
|
||
|
||
const effectFn = effect(getter, {
|
||
lazy: true,
|
||
scheduler() {
|
||
if (!dirty) {
|
||
dirty = true
|
||
trigger(obj, 'value') // 触发依赖此 computed 的更新
|
||
}
|
||
}
|
||
})
|
||
|
||
const obj = {
|
||
get value() {
|
||
if (dirty) {
|
||
value = effectFn()
|
||
dirty = false
|
||
}
|
||
track(obj, 'value') // computed 也要收集依赖
|
||
return value
|
||
},
|
||
set value(newVal) {
|
||
setter(newVal)
|
||
}
|
||
}
|
||
|
||
return obj
|
||
}
|
||
```
|
||
|
||
**缓存机制核心:**
|
||
|
||
1. **dirty 标志**:标记是否需要重新计算
|
||
2. **惰性求值**:只有访问 `.value` 时才计算
|
||
3. **依赖变化时**:只将 dirty 设为 true,不立即计算
|
||
4. **再次访问时**:发现 dirty 为 true,重新计算
|
||
|
||
**面试加分点:**
|
||
```javascript
|
||
// computed 也支持 debug
|
||
const plusOne = computed(() => count.value + 1, {
|
||
onTrack(e) {
|
||
debugger
|
||
},
|
||
onTrigger(e) {
|
||
debugger
|
||
}
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 虚拟 DOM 与 Diff 算法
|
||
|
||
### Q8: Vue 3 的 Diff 算法相比 Vue 2 有哪些优化?
|
||
|
||
**答案:**
|
||
|
||
**Vue 2 Diff(双端比较):**
|
||
```javascript
|
||
// 同时从新旧子节点的两端开始比较
|
||
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
|
||
// 头头、尾尾、头尾、尾头 四种比较
|
||
}
|
||
```
|
||
|
||
**Vue 3 Diff(快速 Diff + 最长递增子序列):**
|
||
|
||
```javascript
|
||
// 1. 预处理:头部相同节点
|
||
while (i <= e1 && i <= e2) {
|
||
if (isSameVNodeType(n1[i], n2[i])) {
|
||
patch(n1[i], n2[i], container)
|
||
i++
|
||
} else break
|
||
}
|
||
|
||
// 2. 预处理:尾部相同节点
|
||
while (i <= e1 && i <= e2) {
|
||
if (isSameVNodeType(n1[e1], n2[e2])) {
|
||
patch(n1[e1], n2[e2], container)
|
||
e1--
|
||
e2--
|
||
} else break
|
||
}
|
||
|
||
// 3. 新节点有剩余 → 新增
|
||
// 4. 旧节点有剩余 → 删除
|
||
// 5. 乱序情况 → 使用最长递增子序列(LIS)最小化移动
|
||
```
|
||
|
||
**最长递增子序列(LIS)优化:**
|
||
|
||
```javascript
|
||
// 旧节点: [a, b, c, d, e, f, g]
|
||
// 新节点: [a, b, e, c, d, f, g]
|
||
//
|
||
// 建立新节点索引映射后:
|
||
// 新位置数组: [4, 2, 3, 5](对应 e, c, d, f 在新数组中的位置)
|
||
// LIS: [2, 3, 5](对应 c, d, f)
|
||
//
|
||
// 只需要移动不在 LIS 中的节点(e),其他节点保持不动
|
||
```
|
||
|
||
**优化效果:**
|
||
|
||
| 场景 | Vue 2 | Vue 3 |
|
||
|------|-------|-------|
|
||
| 头部连续相同 | O(n) | O(1) 跳过 |
|
||
| 尾部连续相同 | O(n) | O(1) 跳过 |
|
||
| 乱序移动 | 可能多余移动 | 最少移动(LIS) |
|
||
|
||
---
|
||
|
||
### Q9: 什么是 PatchFlags?它如何提升性能?
|
||
|
||
**答案:**
|
||
|
||
PatchFlags 是 Vue 3 编译时生成的优化标记,用于运行时快速判断节点需要更新的部分。
|
||
|
||
```javascript
|
||
// 模板
|
||
<div>
|
||
<span>静态文本</span>
|
||
<span>{{ dynamic }}</span>
|
||
<span :class="cls">动态 class</span>
|
||
<span :id="id" :class="cls">多个动态属性</span>
|
||
</div>
|
||
|
||
// 编译后(简化)
|
||
import { createVNode as _createVNode } from 'vue'
|
||
|
||
// PatchFlag 枚举
|
||
const PatchFlags = {
|
||
TEXT: 1, // 动态文本
|
||
CLASS: 2, // 动态 class
|
||
STYLE: 4, // 动态 style
|
||
PROPS: 8, // 动态属性(非 class/style)
|
||
FULL_PROPS: 16, // 有动态 key 的属性
|
||
HYDRATE_EVENTS: 32,// 需要事件监听器
|
||
STABLE_FRAGMENT: 64,
|
||
KEYED_FRAGMENT: 128,
|
||
UNKEYED_FRAGMENT: 256,
|
||
NEED_PATCH: 512, // ref/指令
|
||
DYNAMIC_SLOTS: 1024,
|
||
HOISTED: -1, // 静态提升
|
||
BAIL: -2 // 跳出优化模式
|
||
}
|
||
|
||
_createVNode('span', null, dynamic, PatchFlags.TEXT)
|
||
_createVNode('span', { class: cls }, '动态 class', PatchFlags.CLASS)
|
||
```
|
||
|
||
**优化效果:**
|
||
|
||
```javascript
|
||
// 运行时 patch 函数
|
||
function patchElement(n1, n2) {
|
||
const patchFlag = n2.patchFlag
|
||
|
||
if (patchFlag > 0) {
|
||
// 有 patchFlag,走快速路径
|
||
if (patchFlag & PatchFlags.CLASS) {
|
||
// 只更新 class
|
||
patchClass(el, n2.props.class)
|
||
}
|
||
if (patchFlag & PatchFlags.STYLE) {
|
||
// 只更新 style
|
||
patchStyle(el, n1.props.style, n2.props.style)
|
||
}
|
||
// ... 其他按需更新
|
||
} else {
|
||
// 没有 patchFlag,全量 diff
|
||
patchProps(el, n1.props, n2.props)
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 编译优化
|
||
|
||
### Q10: 请解释 Vue 3 的静态提升(Static Hoisting)
|
||
|
||
**答案:**
|
||
|
||
静态提升是将模板中的静态内容提升到 render 函数外部,避免每次渲染时重新创建。
|
||
|
||
```javascript
|
||
// 模板
|
||
<template>
|
||
<div>
|
||
<span class="static">静态内容</span>
|
||
<span>{{ dynamic }}</span>
|
||
</div>
|
||
</template>
|
||
|
||
// Vue 2 编译结果(每次渲染都创建)
|
||
function render() {
|
||
return h('div', [
|
||
h('span', { class: 'static' }, '静态内容'),
|
||
h('span', this.dynamic)
|
||
])
|
||
}
|
||
|
||
// Vue 3 编译结果(静态提升)
|
||
const _hoisted_1 = h('span', { class: 'static' }, '静态内容')
|
||
|
||
function render() {
|
||
return h('div', [
|
||
_hoisted_1, // 复用同一个 VNode
|
||
h('span', this.dynamic)
|
||
])
|
||
}
|
||
```
|
||
|
||
**提升级别:**
|
||
|
||
1. **元素提升**:完全静态的元素
|
||
2. **props 提升**:静态的 props 对象
|
||
3. **树提升**:连续多个静态节点合并成静态字符串
|
||
|
||
```javascript
|
||
// 多个连续静态节点
|
||
<div>
|
||
<span>1</span>
|
||
<span>2</span>
|
||
<span>3</span>
|
||
<!-- ... 大量静态内容 -->
|
||
</div>
|
||
|
||
// 编译为静态 HTML 字符串
|
||
const _hoisted = createStaticVNode('<span>1</span><span>2</span><span>3</span>...')
|
||
```
|
||
|
||
---
|
||
|
||
### Q11: 什么是 Block Tree?它如何优化更新性能?
|
||
|
||
**答案:**
|
||
|
||
Block Tree 是 Vue 3 中用于收集动态节点的优化机制,跳过静态节点的 diff。
|
||
|
||
```javascript
|
||
// 模板
|
||
<div>
|
||
<span>静态</span>
|
||
<span>静态</span>
|
||
<span>{{ msg }}</span>
|
||
<div>
|
||
<span>静态</span>
|
||
<span :class="cls">动态</span>
|
||
</div>
|
||
</div>
|
||
|
||
// 传统 Diff:遍历整棵树
|
||
// Block Tree:只比较动态节点数组
|
||
|
||
// 编译结果
|
||
function render() {
|
||
return (openBlock(), createBlock('div', null, [
|
||
createVNode('span', null, '静态'),
|
||
createVNode('span', null, '静态'),
|
||
createVNode('span', null, msg, PatchFlags.TEXT),
|
||
createVNode('div', null, [
|
||
createVNode('span', null, '静态'),
|
||
createVNode('span', { class: cls }, '动态', PatchFlags.CLASS)
|
||
])
|
||
]))
|
||
}
|
||
```
|
||
|
||
**Block 的动态节点收集:**
|
||
|
||
```javascript
|
||
// Block 节点会收集所有后代中的动态节点
|
||
const block = {
|
||
type: 'div',
|
||
children: [...],
|
||
dynamicChildren: [
|
||
// 扁平化的动态节点数组
|
||
{ type: 'span', children: msg, patchFlag: TEXT },
|
||
{ type: 'span', props: { class: cls }, patchFlag: CLASS }
|
||
]
|
||
}
|
||
|
||
// 更新时直接遍历 dynamicChildren
|
||
function patchBlock(n1, n2) {
|
||
for (let i = 0; i < n2.dynamicChildren.length; i++) {
|
||
patch(n1.dynamicChildren[i], n2.dynamicChildren[i])
|
||
}
|
||
}
|
||
```
|
||
|
||
**Block 边界:**
|
||
结构不稳定的节点(v-if、v-for)会创建新的 Block:
|
||
|
||
```javascript
|
||
<div> <!-- Block -->
|
||
<div v-if="show"> <!-- Block -->
|
||
<span>{{ msg }}</span>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 生命周期与调度机制
|
||
|
||
### Q12: Vue 3 的组件更新是同步还是异步?请解释调度机制
|
||
|
||
**答案:**
|
||
|
||
Vue 3 的组件更新是**异步批量**的,通过调度器(Scheduler)管理更新时机。
|
||
|
||
```javascript
|
||
// 响应式数据变化时,不会立即更新组件
|
||
const count = ref(0)
|
||
count.value++ // 不会立即触发更新
|
||
count.value++ // 不会立即触发更新
|
||
count.value++ // 只会触发一次更新(批量处理)
|
||
|
||
// 调度器简化实现
|
||
const queue = []
|
||
let isFlushing = false
|
||
let isFlushPending = false
|
||
const resolvedPromise = Promise.resolve()
|
||
|
||
function queueJob(job) {
|
||
if (!queue.includes(job)) {
|
||
queue.push(job)
|
||
}
|
||
queueFlush()
|
||
}
|
||
|
||
function queueFlush() {
|
||
if (!isFlushing && !isFlushPending) {
|
||
isFlushPending = true
|
||
resolvedPromise.then(flushJobs)
|
||
}
|
||
}
|
||
|
||
function flushJobs() {
|
||
isFlushPending = false
|
||
isFlushing = true
|
||
|
||
// 排序:父组件先于子组件更新
|
||
queue.sort((a, b) => a.id - b.id)
|
||
|
||
for (const job of queue) {
|
||
job()
|
||
}
|
||
|
||
queue.length = 0
|
||
isFlushing = false
|
||
|
||
// 检查是否有新的任务加入
|
||
if (queue.length) {
|
||
flushJobs()
|
||
}
|
||
}
|
||
```
|
||
|
||
**三个队列:**
|
||
|
||
```javascript
|
||
// 1. Pre 队列:组件更新前的任务(watchEffect 默认)
|
||
const pendingPreFlushCbs = []
|
||
|
||
// 2. 组件更新队列
|
||
const queue = []
|
||
|
||
// 3. Post 队列:组件更新后的任务(onMounted、watchPostEffect)
|
||
const pendingPostFlushCbs = []
|
||
|
||
// 执行顺序
|
||
async function flushJobs() {
|
||
// 1. 执行 Pre 队列
|
||
flushPreFlushCbs()
|
||
|
||
// 2. 执行组件更新
|
||
for (const job of queue) {
|
||
job()
|
||
}
|
||
|
||
// 3. 执行 Post 队列
|
||
flushPostFlushCbs()
|
||
}
|
||
```
|
||
|
||
**nextTick 原理:**
|
||
|
||
```javascript
|
||
function nextTick(fn) {
|
||
return fn
|
||
? resolvedPromise.then(fn)
|
||
: resolvedPromise
|
||
}
|
||
|
||
// 使用
|
||
await nextTick()
|
||
console.log(document.querySelector('.count').textContent) // 更新后的 DOM
|
||
```
|
||
|
||
---
|
||
|
||
### Q13: 父子组件的生命周期执行顺序是什么?
|
||
|
||
**答案:**
|
||
|
||
```
|
||
挂载阶段:
|
||
父 setup
|
||
父 onBeforeMount
|
||
子 setup
|
||
子 onBeforeMount
|
||
子 onMounted
|
||
父 onMounted
|
||
|
||
更新阶段:
|
||
父 onBeforeUpdate
|
||
子 onBeforeUpdate
|
||
子 onUpdated
|
||
父 onUpdated
|
||
|
||
卸载阶段:
|
||
父 onBeforeUnmount
|
||
子 onBeforeUnmount
|
||
子 onUnmounted
|
||
父 onUnmounted
|
||
```
|
||
|
||
**记忆口诀:**
|
||
- 挂载:父 before → 子全部 → 父 mounted(子先完成)
|
||
- 更新:父 before → 子全部 → 父 updated(子先完成)
|
||
- 卸载:父 before → 子全部 → 父 unmounted(子先完成)
|
||
|
||
**特殊情况 - 异步组件:**
|
||
|
||
```javascript
|
||
const AsyncComp = defineAsyncComponent(() => import('./Comp.vue'))
|
||
|
||
// 父组件会先完成挂载,不等待异步子组件
|
||
// 父 mounted → 异步加载 → 子 mounted
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 组件通信与状态管理
|
||
|
||
### Q14: Vue 3 有哪些组件通信方式?各自的使用场景是什么?
|
||
|
||
**答案:**
|
||
|
||
```javascript
|
||
// 1. Props / Emit(父子组件)
|
||
// 父组件
|
||
<Child :count="count" @update="handleUpdate" />
|
||
|
||
// 子组件
|
||
const props = defineProps(['count'])
|
||
const emit = defineEmits(['update'])
|
||
emit('update', newValue)
|
||
|
||
// 2. v-model(双向绑定语法糖)
|
||
// Vue 3 支持多个 v-model
|
||
<Child v-model="value" v-model:title="title" />
|
||
|
||
// 子组件
|
||
defineProps(['modelValue', 'title'])
|
||
defineEmits(['update:modelValue', 'update:title'])
|
||
|
||
// 3. Provide / Inject(跨层级)
|
||
// 祖先组件
|
||
provide('theme', ref('dark'))
|
||
|
||
// 后代组件
|
||
const theme = inject('theme')
|
||
|
||
// 4. Expose / Ref(父访问子)
|
||
// 子组件
|
||
defineExpose({
|
||
childMethod: () => console.log('called from parent')
|
||
})
|
||
|
||
// 父组件
|
||
const childRef = ref(null)
|
||
childRef.value.childMethod()
|
||
|
||
// 5. Attrs / Slots(透传)
|
||
const { attrs, slots } = useAttrs(), useSlots()
|
||
|
||
// 6. EventBus(任意组件,Vue 3 推荐 mitt)
|
||
import mitt from 'mitt'
|
||
const emitter = mitt()
|
||
emitter.emit('event', data)
|
||
emitter.on('event', handler)
|
||
|
||
// 7. Pinia(全局状态管理)
|
||
export const useStore = defineStore('main', () => {
|
||
const count = ref(0)
|
||
const increment = () => count.value++
|
||
return { count, increment }
|
||
})
|
||
```
|
||
|
||
**选择指南:**
|
||
|
||
| 场景 | 推荐方案 |
|
||
|------|----------|
|
||
| 父→子 | props |
|
||
| 子→父 | emit |
|
||
| 双向绑定 | v-model |
|
||
| 深层传递 | provide/inject |
|
||
| 父调用子方法 | expose + ref |
|
||
| 全局状态 | Pinia |
|
||
| 兄弟/任意组件 | Pinia 或 mitt |
|
||
|
||
---
|
||
|
||
### Q15: Pinia 相比 Vuex 有哪些优势?请解释其核心原理
|
||
|
||
**答案:**
|
||
|
||
**优势对比:**
|
||
|
||
| 特性 | Vuex | Pinia |
|
||
|------|------|-------|
|
||
| TypeScript 支持 | 需要额外配置 | 完美支持 |
|
||
| 模块化 | 需要 modules | 天然多 store |
|
||
| Mutations | 必须有 | 移除了 |
|
||
| Devtools | 支持 | 支持 |
|
||
| 代码体积 | 较大 | 约 1KB |
|
||
| Composition API | 需要 mapState 等 | 原生支持 |
|
||
|
||
**Pinia 核心实现原理:**
|
||
|
||
```javascript
|
||
// 简化版 Pinia 实现
|
||
import { reactive, effectScope, computed } from 'vue'
|
||
|
||
function createPinia() {
|
||
const stores = new Map()
|
||
|
||
return {
|
||
_stores: stores,
|
||
install(app) {
|
||
app.provide('pinia', this)
|
||
}
|
||
}
|
||
}
|
||
|
||
function defineStore(id, setup) {
|
||
return function useStore() {
|
||
const pinia = inject('pinia')
|
||
|
||
if (!pinia._stores.has(id)) {
|
||
// 使用 effectScope 管理副作用
|
||
const scope = effectScope()
|
||
const store = scope.run(() => {
|
||
const setupResult = setup()
|
||
return reactive(setupResult)
|
||
})
|
||
|
||
pinia._stores.set(id, store)
|
||
}
|
||
|
||
return pinia._stores.get(id)
|
||
}
|
||
}
|
||
|
||
// 使用
|
||
const useCounterStore = defineStore('counter', () => {
|
||
const count = ref(0)
|
||
const double = computed(() => count.value * 2)
|
||
const increment = () => count.value++
|
||
|
||
return { count, double, increment }
|
||
})
|
||
```
|
||
|
||
**核心概念:**
|
||
|
||
1. **响应式状态**:使用 Vue 的响应式系统
|
||
2. **effectScope**:管理 store 中的副作用(watch、computed)
|
||
3. **单例模式**:同一个 store 只创建一次
|
||
4. **支持 SSR**:通过 pinia.state.value 实现状态序列化
|
||
|
||
---
|
||
|
||
## 7. 性能优化
|
||
|
||
### Q16: Vue 3 有哪些性能优化手段?
|
||
|
||
**答案:**
|
||
|
||
**编译时优化(自动):**
|
||
|
||
```javascript
|
||
// 1. 静态提升
|
||
const _hoisted = createVNode('span', null, '静态内容')
|
||
|
||
// 2. PatchFlags 标记
|
||
createVNode('span', null, msg, 1 /* TEXT */)
|
||
|
||
// 3. Block Tree
|
||
openBlock()
|
||
createBlock('div', null, [...])
|
||
|
||
// 4. 缓存事件处理函数
|
||
onClick: _cache[0] || (_cache[0] = $event => handler($event))
|
||
```
|
||
|
||
**运行时优化(手动):**
|
||
|
||
```javascript
|
||
// 1. v-once:只渲染一次
|
||
<span v-once>{{ expensiveComputation }}</span>
|
||
|
||
// 2. v-memo:条件缓存
|
||
<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
|
||
<!-- 只有 selected 变化才重新渲染 -->
|
||
</div>
|
||
|
||
// 3. shallowRef / shallowReactive:浅层响应式
|
||
const state = shallowRef({ nested: { count: 0 } })
|
||
// state.value.nested.count 的变化不会触发更新
|
||
|
||
// 4. markRaw:标记不需要响应式
|
||
const rawData = markRaw({ huge: 'data' })
|
||
|
||
// 5. 组件懒加载
|
||
const AsyncComp = defineAsyncComponent(() =>
|
||
import('./HeavyComponent.vue')
|
||
)
|
||
|
||
// 6. keep-alive 缓存
|
||
<keep-alive :include="['ComponentA']" :max="10">
|
||
<component :is="currentComponent" />
|
||
</keep-alive>
|
||
|
||
// 7. 虚拟滚动(大列表)
|
||
import { VirtualList } from 'vue-virtual-scroller'
|
||
|
||
// 8. 合理使用 computed 缓存
|
||
const filtered = computed(() =>
|
||
list.value.filter(item => item.active)
|
||
)
|
||
|
||
// 9. 避免不必要的响应式
|
||
// ❌ 大对象整体响应式
|
||
const state = reactive(hugeObject)
|
||
// ✅ 只对需要的部分响应式
|
||
const selectedId = ref(null)
|
||
```
|
||
|
||
**打包优化:**
|
||
|
||
```javascript
|
||
// 1. Tree-shaking(Vue 3 API 按需引入)
|
||
import { ref, computed } from 'vue'
|
||
|
||
// 2. 路由懒加载
|
||
const routes = [
|
||
{ path: '/home', component: () => import('./views/Home.vue') }
|
||
]
|
||
|
||
// 3. 分包策略
|
||
// vite.config.js
|
||
build: {
|
||
rollupOptions: {
|
||
output: {
|
||
manualChunks: {
|
||
'vue-vendor': ['vue', 'vue-router', 'pinia']
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Q17: 什么情况下会导致 Vue 组件重复渲染?如何避免?
|
||
|
||
**答案:**
|
||
|
||
**常见原因及解决方案:**
|
||
|
||
```javascript
|
||
// 1. ❌ 内联对象/函数(每次渲染都是新引用)
|
||
<Child :config="{ a: 1 }" @click="() => handleClick()" />
|
||
|
||
// ✅ 使用 computed 或提取为变量
|
||
const config = computed(() => ({ a: 1 }))
|
||
const handleClick = () => { /* ... */ }
|
||
<Child :config="config" @click="handleClick" />
|
||
|
||
// 2. ❌ v-for 没有 key 或使用 index
|
||
<div v-for="(item, index) in list" :key="index">
|
||
|
||
// ✅ 使用唯一标识
|
||
<div v-for="item in list" :key="item.id">
|
||
|
||
// 3. ❌ 不必要的响应式依赖
|
||
const doubled = computed(() => {
|
||
console.log('computed') // 每次 otherValue 变化也会重算
|
||
return props.value * 2 + otherValue.value * 0
|
||
})
|
||
|
||
// ✅ 只依赖必要的数据
|
||
const doubled = computed(() => props.value * 2)
|
||
|
||
// 4. ❌ 在模板中调用方法(每次渲染都执行)
|
||
<span>{{ formatDate(date) }}</span>
|
||
|
||
// ✅ 使用 computed
|
||
const formattedDate = computed(() => formatDate(date.value))
|
||
|
||
// 5. ❌ 父组件状态变化导致子组件不必要更新
|
||
// ✅ 使用 v-memo
|
||
<Child v-memo="[item.id, item.selected]" :item="item" />
|
||
|
||
// 6. ❌ 监听器触发不必要的状态更新
|
||
watch(source, () => {
|
||
state.value++ // 可能引发连锁更新
|
||
})
|
||
|
||
// ✅ 检查是否真的需要更新
|
||
watch(source, (newVal, oldVal) => {
|
||
if (needsUpdate(newVal, oldVal)) {
|
||
state.value++
|
||
}
|
||
})
|
||
```
|
||
|
||
**调试方法:**
|
||
|
||
```javascript
|
||
// 使用 onRenderTracked / onRenderTriggered
|
||
import { onRenderTracked, onRenderTriggered } from 'vue'
|
||
|
||
onRenderTracked((event) => {
|
||
console.log('依赖收集:', event)
|
||
})
|
||
|
||
onRenderTriggered((event) => {
|
||
console.log('触发更新:', event)
|
||
})
|
||
|
||
// Vue Devtools 的 Performance 面板
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Vapor Mode 深度解析
|
||
|
||
### Q18: 什么是 Vapor Mode?它的核心原理是什么?
|
||
|
||
**答案:**
|
||
|
||
Vapor Mode 是 Vue 团队开发的一种**无虚拟 DOM**的编译策略,通过编译时将模板直接转换为命令式 DOM 操作,绕过虚拟 DOM 和 Diff 算法,实现更高的运行时性能。
|
||
|
||
**核心理念:**
|
||
|
||
```javascript
|
||
// 传统 Vue(Virtual DOM 模式)
|
||
// 模板 → 渲染函数 → VNode → Diff → DOM 操作
|
||
|
||
// Vapor Mode
|
||
// 模板 → 命令式 DOM 操作代码(直接操作 DOM)
|
||
```
|
||
|
||
**编译对比示例:**
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<span>静态文本</span>
|
||
<span>{{ count }}</span>
|
||
<button @click="increment">+1</button>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
**传统模式编译结果:**
|
||
|
||
```javascript
|
||
import { createVNode, openBlock, createBlock, toDisplayString } from 'vue'
|
||
|
||
function render(_ctx) {
|
||
return (openBlock(), createBlock('div', null, [
|
||
createVNode('span', null, '静态文本'),
|
||
createVNode('span', null, toDisplayString(_ctx.count), 1 /* TEXT */),
|
||
createVNode('button', { onClick: _ctx.increment }, '+1')
|
||
]))
|
||
}
|
||
```
|
||
|
||
**Vapor Mode 编译结果:**
|
||
|
||
```javascript
|
||
import { template, effect, setText, on } from 'vue/vapor'
|
||
|
||
// 1. 创建静态模板(一次性)
|
||
const t0 = template('<div><span>静态文本</span><span></span><button>+1</button></div>')
|
||
|
||
function render(_ctx) {
|
||
// 2. 克隆模板
|
||
const root = t0()
|
||
|
||
// 3. 获取动态节点引用
|
||
const span1 = root.firstChild.nextSibling
|
||
const button = span1.nextSibling
|
||
|
||
// 4. 建立响应式绑定(细粒度更新)
|
||
effect(() => {
|
||
setText(span1, _ctx.count)
|
||
})
|
||
|
||
// 5. 绑定事件
|
||
on(button, 'click', _ctx.increment)
|
||
|
||
return root
|
||
}
|
||
```
|
||
|
||
**关键差异:**
|
||
|
||
| 特性 | Virtual DOM 模式 | Vapor Mode |
|
||
|------|-----------------|------------|
|
||
| 更新机制 | 创建新 VNode → Diff → Patch | 直接更新目标 DOM 节点 |
|
||
| 内存占用 | 需要维护 VNode 树 | 无 VNode 开销 |
|
||
| 更新粒度 | 组件级别 | 节点级别(细粒度) |
|
||
| 运行时大小 | 完整运行时 (~50KB) | 精简运行时 (~6KB) |
|
||
| 适用场景 | 通用场景 | 性能敏感场景 |
|
||
|
||
---
|
||
|
||
### Q19: Vapor Mode 的细粒度响应式更新是如何实现的?
|
||
|
||
**答案:**
|
||
|
||
Vapor Mode 结合了 Vue 的响应式系统和 SolidJS 的细粒度更新思想,为每个动态绑定创建独立的 effect。
|
||
|
||
**实现原理:**
|
||
|
||
```javascript
|
||
// Vapor 运行时核心简化实现
|
||
import { effect as rawEffect } from '@vue/reactivity'
|
||
|
||
// 封装 effect,支持调度
|
||
function effect(fn) {
|
||
const effectFn = rawEffect(fn, {
|
||
scheduler: () => {
|
||
// 使用微任务批量调度,避免同步更新过多
|
||
queueMicrotask(effectFn)
|
||
}
|
||
})
|
||
return effectFn
|
||
}
|
||
|
||
// 文本更新
|
||
function setText(el, value) {
|
||
el.textContent = value
|
||
}
|
||
|
||
// 属性更新
|
||
function setAttr(el, key, value) {
|
||
if (value == null) {
|
||
el.removeAttribute(key)
|
||
} else {
|
||
el.setAttribute(key, value)
|
||
}
|
||
}
|
||
|
||
// 类名更新
|
||
function setClass(el, value) {
|
||
el.className = value
|
||
}
|
||
|
||
// 样式更新
|
||
function setStyle(el, key, value) {
|
||
el.style[key] = value
|
||
}
|
||
|
||
// 模板克隆(高性能)
|
||
function template(html) {
|
||
const tpl = document.createElement('template')
|
||
tpl.innerHTML = html
|
||
return () => tpl.content.firstChild.cloneNode(true)
|
||
}
|
||
```
|
||
|
||
**细粒度更新流程:**
|
||
|
||
```javascript
|
||
// 假设有以下模板
|
||
<div :class="cls" :style="{ color }">
|
||
<span>{{ firstName }}</span>
|
||
<span>{{ lastName }}</span>
|
||
</div>
|
||
|
||
// Vapor 编译后,每个动态绑定都有独立的 effect
|
||
function render(_ctx) {
|
||
const root = t0()
|
||
const span1 = root.firstChild
|
||
const span2 = span1.nextSibling
|
||
|
||
// 独立的 effect:firstName 变化只更新 span1
|
||
effect(() => setText(span1, _ctx.firstName))
|
||
|
||
// 独立的 effect:lastName 变化只更新 span2
|
||
effect(() => setText(span2, _ctx.lastName))
|
||
|
||
// 独立的 effect:cls 变化只更新 class
|
||
effect(() => setClass(root, _ctx.cls))
|
||
|
||
// 独立的 effect:color 变化只更新 style
|
||
effect(() => setStyle(root, 'color', _ctx.color))
|
||
|
||
return root
|
||
}
|
||
|
||
// 当 firstName 变化时:
|
||
// - 只有 span1 的 effect 被触发
|
||
// - 直接执行 setText(span1, newValue)
|
||
// - 没有 VNode 创建,没有 Diff 比较
|
||
```
|
||
|
||
---
|
||
|
||
### Q20: Vapor Mode 如何处理条件渲染和列表渲染?
|
||
|
||
**答案:**
|
||
|
||
**条件渲染 (v-if):**
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<span v-if="show">显示</span>
|
||
<span v-else>隐藏</span>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
```javascript
|
||
// Vapor 编译结果
|
||
import { template, effect, createIf } from 'vue/vapor'
|
||
|
||
const t0 = template('<div></div>')
|
||
const t1 = template('<span>显示</span>')
|
||
const t2 = template('<span>隐藏</span>')
|
||
|
||
function render(_ctx) {
|
||
const root = t0()
|
||
|
||
// createIf 返回一个响应式的条件块
|
||
const ifBlock = createIf(
|
||
() => _ctx.show, // 条件
|
||
() => t1(), // true 分支
|
||
() => t2() // false 分支(v-else)
|
||
)
|
||
|
||
// 插入条件块
|
||
root.appendChild(ifBlock.anchor)
|
||
|
||
return root
|
||
}
|
||
|
||
// createIf 简化实现
|
||
function createIf(condition, trueBranch, falseBranch) {
|
||
const anchor = document.createComment('v-if')
|
||
let currentBranch = null
|
||
let currentNode = null
|
||
|
||
effect(() => {
|
||
const shouldShow = condition()
|
||
const newBranch = shouldShow ? trueBranch : falseBranch
|
||
|
||
if (newBranch !== currentBranch) {
|
||
// 移除旧节点
|
||
if (currentNode) {
|
||
currentNode.remove()
|
||
}
|
||
// 插入新节点
|
||
currentNode = newBranch?.()
|
||
if (currentNode) {
|
||
anchor.parentNode.insertBefore(currentNode, anchor)
|
||
}
|
||
currentBranch = newBranch
|
||
}
|
||
})
|
||
|
||
return { anchor }
|
||
}
|
||
```
|
||
|
||
**列表渲染 (v-for):**
|
||
|
||
```vue
|
||
<template>
|
||
<ul>
|
||
<li v-for="item in items" :key="item.id">
|
||
{{ item.name }}
|
||
</li>
|
||
</ul>
|
||
</template>
|
||
```
|
||
|
||
```javascript
|
||
// Vapor 编译结果
|
||
import { template, effect, createFor } from 'vue/vapor'
|
||
|
||
const t0 = template('<ul></ul>')
|
||
const t1 = template('<li></li>')
|
||
|
||
function render(_ctx) {
|
||
const root = t0()
|
||
|
||
const forBlock = createFor(
|
||
() => _ctx.items, // 数据源
|
||
(item, index) => { // 每项的渲染函数
|
||
const li = t1()
|
||
effect(() => setText(li, item.value.name))
|
||
return li
|
||
},
|
||
(item) => item.id // key 函数
|
||
)
|
||
|
||
root.appendChild(forBlock.anchor)
|
||
|
||
return root
|
||
}
|
||
|
||
// createFor 简化实现(使用 Map 复用节点)
|
||
function createFor(source, renderItem, getKey) {
|
||
const anchor = document.createComment('v-for')
|
||
const keyToNode = new Map()
|
||
let oldKeys = []
|
||
|
||
effect(() => {
|
||
const items = source()
|
||
const newKeys = items.map(getKey)
|
||
const newKeyToNode = new Map()
|
||
|
||
// 1. 复用已有节点或创建新节点
|
||
items.forEach((item, index) => {
|
||
const key = getKey(item)
|
||
let node = keyToNode.get(key)
|
||
|
||
if (!node) {
|
||
// 创建新节点
|
||
node = renderItem(ref(item), ref(index))
|
||
}
|
||
newKeyToNode.set(key, node)
|
||
})
|
||
|
||
// 2. 移除不再需要的节点
|
||
oldKeys.forEach(key => {
|
||
if (!newKeyToNode.has(key)) {
|
||
keyToNode.get(key).remove()
|
||
}
|
||
})
|
||
|
||
// 3. 按新顺序插入节点(使用最少移动算法)
|
||
reorderNodes(anchor.parentNode, anchor, items.map(i => newKeyToNode.get(getKey(i))))
|
||
|
||
keyToNode.clear()
|
||
newKeyToNode.forEach((v, k) => keyToNode.set(k, v))
|
||
oldKeys = newKeys
|
||
})
|
||
|
||
return { anchor }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Q21: Vapor Mode 与 Virtual DOM 模式可以混合使用吗?
|
||
|
||
**答案:**
|
||
|
||
**可以!** Vue 的设计允许在同一应用中混合使用两种模式,这是 Vapor Mode 的重要特性。
|
||
|
||
**混合使用场景:**
|
||
|
||
```javascript
|
||
// vite.config.js
|
||
import { defineConfig } from 'vite'
|
||
import vue from '@vitejs/plugin-vue'
|
||
|
||
export default defineConfig({
|
||
plugins: [
|
||
vue({
|
||
vapor: true // 启用 Vapor Mode 支持
|
||
})
|
||
]
|
||
})
|
||
```
|
||
|
||
```vue
|
||
<!-- App.vue(Virtual DOM 模式) -->
|
||
<template>
|
||
<div>
|
||
<Header />
|
||
<VaporComponent /> <!-- Vapor Mode 组件 -->
|
||
<Footer />
|
||
</div>
|
||
</template>
|
||
|
||
<!-- VaporComponent.vapor.vue(Vapor Mode) -->
|
||
<!-- 通过 .vapor.vue 后缀标识 -->
|
||
<template>
|
||
<div>
|
||
<span>{{ count }}</span>
|
||
<button @click="increment">+1</button>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
const count = ref(0)
|
||
const increment = () => count.value++
|
||
</script>
|
||
```
|
||
|
||
**互操作机制:**
|
||
|
||
```javascript
|
||
// Vapor 组件在 VDOM 中的表现
|
||
// Vue 会为 Vapor 组件创建一个特殊的 VNode 包装器
|
||
|
||
const VaporComponentVNode = {
|
||
type: VaporComponent,
|
||
__vapor: true, // 标记为 Vapor 组件
|
||
|
||
// 挂载时直接调用 Vapor 渲染函数
|
||
mount(container) {
|
||
const instance = createVaporInstance(VaporComponent)
|
||
const root = instance.render()
|
||
container.appendChild(root)
|
||
},
|
||
|
||
// 更新由 Vapor 的响应式系统自动处理
|
||
// VDOM 侧不需要 diff 这个组件的内部
|
||
}
|
||
```
|
||
|
||
**选择策略:**
|
||
|
||
| 组件类型 | 推荐模式 | 原因 |
|
||
|----------|----------|------|
|
||
| 性能关键组件 | Vapor | 细粒度更新,零 VDOM 开销 |
|
||
| 高度动态组件 | VDOM | 更灵活的动态渲染 |
|
||
| 第三方组件库 | VDOM | 兼容性 |
|
||
| 简单展示组件 | Vapor | 更小的运行时 |
|
||
| 复杂状态组件 | 视情况 | 评估更新频率和模式 |
|
||
|
||
---
|
||
|
||
### Q22: Vapor Mode 的优势、局限性和适用场景是什么?
|
||
|
||
**答案:**
|
||
|
||
**优势:**
|
||
|
||
```javascript
|
||
// 1. 更小的运行时体积
|
||
// VDOM 运行时: ~50KB (gzip ~16KB)
|
||
// Vapor 运行时: ~6KB (gzip ~2KB)
|
||
|
||
// 2. 更快的更新性能
|
||
// 基准测试(更新 1000 行表格):
|
||
// VDOM: ~15ms
|
||
// Vapor: ~3ms(快 5 倍)
|
||
|
||
// 3. 更低的内存占用
|
||
// 无 VNode 对象创建,GC 压力更小
|
||
|
||
// 4. 更快的首次渲染
|
||
// 无需创建完整的 VNode 树
|
||
```
|
||
|
||
**局限性:**
|
||
|
||
```javascript
|
||
// 1. 动态组件支持有限
|
||
// ❌ 不支持
|
||
<component :is="dynamicComponent" />
|
||
|
||
// 2. 渲染函数/JSX 不支持
|
||
// Vapor 依赖模板编译,不支持手写渲染函数
|
||
export default {
|
||
render() {
|
||
return h('div', this.msg) // ❌ 不能使用 Vapor
|
||
}
|
||
}
|
||
|
||
// 3. 部分动态指令受限
|
||
// 需要编译时确定的指令
|
||
|
||
// 4. 生态兼容性
|
||
// 部分依赖 VDOM 的组件库可能不兼容
|
||
```
|
||
|
||
**适用场景评估:**
|
||
|
||
```javascript
|
||
// ✅ 推荐使用 Vapor Mode
|
||
- 性能敏感的移动端应用
|
||
- 大量数据展示的表格/列表
|
||
- 嵌入式/资源受限环境
|
||
- 追求极致首屏性能的场景
|
||
- 简单的交互组件
|
||
|
||
// ⚠️ 谨慎使用
|
||
- 高度动态的组件结构
|
||
- 需要手写渲染函数的场景
|
||
- 重度依赖第三方 UI 库
|
||
|
||
// ❌ 不适合
|
||
- 需要 JSX 的项目
|
||
- 动态组件为核心的应用
|
||
```
|
||
|
||
**性能基准对比:**
|
||
|
||
```
|
||
操作类型 VDOM Vapor 提升
|
||
─────────────────────────────────────────────
|
||
创建 1000 行 45ms 12ms 3.8x
|
||
更新全部行 38ms 8ms 4.8x
|
||
更新单行 4ms 0.3ms 13.3x
|
||
交换两行 6ms 0.8ms 7.5x
|
||
删除行 8ms 2ms 4.0x
|
||
内存占用 (1000行) 12MB 3MB 4.0x
|
||
运行时体积 50KB 6KB 8.3x
|
||
```
|
||
|
||
---
|
||
|
||
### Q23: 如何在项目中渐进式采用 Vapor Mode?
|
||
|
||
**答案:**
|
||
|
||
**步骤 1:升级依赖**
|
||
|
||
```bash
|
||
# 确保 Vue 版本支持 Vapor(3.5+)
|
||
npm install vue@latest
|
||
npm install @vitejs/plugin-vue@latest
|
||
```
|
||
|
||
**步骤 2:配置构建工具**
|
||
|
||
```javascript
|
||
// vite.config.js
|
||
import { defineConfig } from 'vite'
|
||
import vue from '@vitejs/plugin-vue'
|
||
|
||
export default defineConfig({
|
||
plugins: [
|
||
vue({
|
||
vapor: true,
|
||
// 可选:指定 Vapor 组件的文件模式
|
||
vaporPatterns: [
|
||
'**/*.vapor.vue',
|
||
'**/vapor/**/*.vue'
|
||
]
|
||
})
|
||
]
|
||
})
|
||
```
|
||
|
||
**步骤 3:识别适合迁移的组件**
|
||
|
||
```javascript
|
||
// 分析工具:识别性能热点
|
||
// 1. 使用 Vue Devtools Performance 面板
|
||
// 2. 找出频繁更新的组件
|
||
// 3. 评估组件复杂度
|
||
|
||
// 适合迁移的特征:
|
||
// - 纯展示组件
|
||
// - 列表/表格组件
|
||
// - 频繁更新的状态展示
|
||
// - 不依赖动态组件/渲染函数
|
||
```
|
||
|
||
**步骤 4:逐步迁移**
|
||
|
||
```vue
|
||
<!-- 原组件:DataTable.vue -->
|
||
<!-- 新组件:DataTable.vapor.vue -->
|
||
|
||
<template>
|
||
<table>
|
||
<tr v-for="row in rows" :key="row.id">
|
||
<td v-for="cell in row.cells" :key="cell.id">
|
||
{{ cell.value }}
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</template>
|
||
|
||
<script setup>
|
||
// Composition API 代码无需修改
|
||
import { ref, computed } from 'vue'
|
||
defineProps(['rows'])
|
||
</script>
|
||
```
|
||
|
||
**步骤 5:性能验证**
|
||
|
||
```javascript
|
||
// 添加性能监控
|
||
import { onMounted, onUpdated } from 'vue'
|
||
|
||
let updateStart = 0
|
||
|
||
onBeforeUpdate(() => {
|
||
updateStart = performance.now()
|
||
})
|
||
|
||
onUpdated(() => {
|
||
const duration = performance.now() - updateStart
|
||
console.log(`Update took: ${duration.toFixed(2)}ms`)
|
||
|
||
// 上报到监控系统
|
||
reportMetric('component_update', duration)
|
||
})
|
||
```
|
||
|
||
**迁移策略建议:**
|
||
|
||
```
|
||
阶段 1(试点)
|
||
├── 选择 1-2 个性能关键组件
|
||
├── 转换为 Vapor Mode
|
||
└── 验证性能提升和功能正确性
|
||
|
||
阶段 2(扩展)
|
||
├── 迁移更多展示型组件
|
||
├── 建立 Vapor 组件开发规范
|
||
└── 团队培训
|
||
|
||
阶段 3(优化)
|
||
├── 监控生产环境性能
|
||
├── 根据数据调整策略
|
||
└── 持续迭代优化
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Vue 3 与 Vue 2 核心区别
|
||
|
||
### Q24: Vue 3 相比 Vue 2 做了哪些重大改进?
|
||
|
||
**答案:**
|
||
|
||
**1. 架构层面:**
|
||
|
||
| 改进点 | Vue 2 | Vue 3 |
|
||
|--------|-------|-------|
|
||
| 响应式系统 | Object.defineProperty | Proxy |
|
||
| 代码组织 | Options API | Composition API |
|
||
| 源码结构 | 单一仓库 | Monorepo(@vue/reactivity 可独立使用) |
|
||
| TypeScript | 额外配置 | 原生支持 |
|
||
| Tree-shaking | 有限 | 全面支持 |
|
||
|
||
**2. 新增特性:**
|
||
|
||
```javascript
|
||
// Teleport:传送组件到任意 DOM 位置
|
||
<Teleport to="body">
|
||
<Modal />
|
||
</Teleport>
|
||
|
||
// Suspense:异步组件加载状态处理
|
||
<Suspense>
|
||
<template #default>
|
||
<AsyncComponent />
|
||
</template>
|
||
<template #fallback>
|
||
<Loading />
|
||
</template>
|
||
</Suspense>
|
||
|
||
// 多根节点(Fragments)
|
||
<template>
|
||
<header>...</header>
|
||
<main>...</main>
|
||
<footer>...</footer>
|
||
</template>
|
||
|
||
// createRenderer:自定义渲染器
|
||
import { createRenderer } from '@vue/runtime-core'
|
||
const { render } = createRenderer({
|
||
createElement(type) { /* ... */ },
|
||
insert(el, parent) { /* ... */ },
|
||
// ...
|
||
})
|
||
```
|
||
|
||
**3. 破坏性变更:**
|
||
|
||
```javascript
|
||
// v-model 变化
|
||
// Vue 2: value + input
|
||
// Vue 3: modelValue + update:modelValue
|
||
|
||
// 移除的 API
|
||
// $on, $off, $once(使用 mitt 替代)
|
||
// $children(使用 ref + expose)
|
||
// $listeners(合并到 $attrs)
|
||
// filters(使用 computed 或方法)
|
||
|
||
// v-for 和 v-if 优先级
|
||
// Vue 2: v-for 优先
|
||
// Vue 3: v-if 优先
|
||
|
||
// 生命周期重命名
|
||
// destroyed → unmounted
|
||
// beforeDestroy → beforeUnmount
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 实战场景题
|
||
|
||
### Q25: 如何实现一个防抖的搜索输入框?
|
||
|
||
**答案:**
|
||
|
||
```javascript
|
||
// 方案1:使用 watchEffect + 自定义防抖
|
||
import { ref, watchEffect } from 'vue'
|
||
|
||
function useDebouncedSearch(delay = 300) {
|
||
const searchTerm = ref('')
|
||
const debouncedTerm = ref('')
|
||
|
||
watchEffect((onCleanup) => {
|
||
const timer = setTimeout(() => {
|
||
debouncedTerm.value = searchTerm.value
|
||
}, delay)
|
||
|
||
onCleanup(() => clearTimeout(timer))
|
||
})
|
||
|
||
return { searchTerm, debouncedTerm }
|
||
}
|
||
|
||
// 使用
|
||
const { searchTerm, debouncedTerm } = useDebouncedSearch(300)
|
||
|
||
watch(debouncedTerm, async (term) => {
|
||
if (term) {
|
||
results.value = await fetchResults(term)
|
||
}
|
||
})
|
||
|
||
// 方案2:使用 VueUse
|
||
import { useDebounceFn, refDebounced } from '@vueuse/core'
|
||
|
||
const searchTerm = ref('')
|
||
const debouncedTerm = refDebounced(searchTerm, 300)
|
||
|
||
// 或者防抖函数
|
||
const debouncedSearch = useDebounceFn((term) => {
|
||
fetchResults(term)
|
||
}, 300)
|
||
|
||
watch(searchTerm, debouncedSearch)
|
||
```
|
||
|
||
---
|
||
|
||
### Q26: 如何封装一个可复用的 useFetch 组合式函数?
|
||
|
||
**答案:**
|
||
|
||
```javascript
|
||
import { ref, shallowRef, watchEffect, toValue } from 'vue'
|
||
|
||
export function useFetch(url, options = {}) {
|
||
const data = shallowRef(null)
|
||
const error = shallowRef(null)
|
||
const loading = ref(false)
|
||
|
||
const execute = async () => {
|
||
loading.value = true
|
||
error.value = null
|
||
|
||
const controller = new AbortController()
|
||
|
||
try {
|
||
const response = await fetch(toValue(url), {
|
||
...options,
|
||
signal: controller.signal
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`)
|
||
}
|
||
|
||
data.value = await response.json()
|
||
} catch (e) {
|
||
if (e.name !== 'AbortError') {
|
||
error.value = e
|
||
}
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
|
||
return controller
|
||
}
|
||
|
||
// 自动执行并支持响应式 URL
|
||
watchEffect((onCleanup) => {
|
||
const controller = execute()
|
||
onCleanup(() => controller?.abort())
|
||
})
|
||
|
||
const refetch = () => execute()
|
||
|
||
return {
|
||
data,
|
||
error,
|
||
loading,
|
||
refetch
|
||
}
|
||
}
|
||
|
||
// 使用
|
||
const userId = ref(1)
|
||
const { data: user, loading, error, refetch } = useFetch(
|
||
() => `/api/users/${userId.value}`
|
||
)
|
||
|
||
// 改变 userId 会自动重新请求
|
||
userId.value = 2
|
||
```
|
||
|
||
---
|
||
|
||
### Q27: 如何实现一个无限滚动列表?
|
||
|
||
**答案:**
|
||
|
||
```javascript
|
||
// useInfiniteScroll.js
|
||
import { ref, onMounted, onUnmounted } from 'vue'
|
||
|
||
export function useInfiniteScroll(loadMore, options = {}) {
|
||
const {
|
||
threshold = 100,
|
||
container = null
|
||
} = options
|
||
|
||
const loading = ref(false)
|
||
const finished = ref(false)
|
||
|
||
const handleScroll = async (e) => {
|
||
if (loading.value || finished.value) return
|
||
|
||
const target = container?.value || document.documentElement
|
||
const scrollHeight = target.scrollHeight
|
||
const scrollTop = target.scrollTop
|
||
const clientHeight = target.clientHeight
|
||
|
||
if (scrollHeight - scrollTop - clientHeight < threshold) {
|
||
loading.value = true
|
||
const hasMore = await loadMore()
|
||
loading.value = false
|
||
|
||
if (!hasMore) {
|
||
finished.value = false
|
||
}
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
const target = container?.value || window
|
||
target.addEventListener('scroll', handleScroll)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
const target = container?.value || window
|
||
target.removeEventListener('scroll', handleScroll)
|
||
})
|
||
|
||
return { loading, finished }
|
||
}
|
||
|
||
// 使用 Intersection Observer 的更优方案
|
||
export function useInfiniteScrollV2(callback) {
|
||
const target = ref(null)
|
||
const loading = ref(false)
|
||
|
||
let observer = null
|
||
|
||
onMounted(() => {
|
||
observer = new IntersectionObserver(async ([entry]) => {
|
||
if (entry.isIntersecting && !loading.value) {
|
||
loading.value = true
|
||
await callback()
|
||
loading.value = false
|
||
}
|
||
})
|
||
|
||
if (target.value) {
|
||
observer.observe(target.value)
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
observer?.disconnect()
|
||
})
|
||
|
||
return { target, loading }
|
||
}
|
||
|
||
// 使用
|
||
<template>
|
||
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
|
||
<div ref="target">
|
||
<span v-if="loading">加载中...</span>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
const items = ref([])
|
||
const page = ref(1)
|
||
|
||
const { target, loading } = useInfiniteScrollV2(async () => {
|
||
const newItems = await fetchItems(page.value++)
|
||
items.value.push(...newItems)
|
||
})
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
### Q28: 如何处理 Vue 3 中的错误边界?
|
||
|
||
**答案:**
|
||
|
||
```javascript
|
||
// ErrorBoundary.vue
|
||
<script setup>
|
||
import { onErrorCaptured, ref } from 'vue'
|
||
|
||
const error = ref(null)
|
||
const errorInfo = ref(null)
|
||
|
||
onErrorCaptured((err, instance, info) => {
|
||
error.value = err
|
||
errorInfo.value = info
|
||
|
||
// 上报错误
|
||
reportError(err, info)
|
||
|
||
// 返回 false 阻止错误继续传播
|
||
return false
|
||
})
|
||
|
||
const reset = () => {
|
||
error.value = null
|
||
errorInfo.value = null
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="error" class="error-boundary">
|
||
<h2>出错了!</h2>
|
||
<p>{{ error.message }}</p>
|
||
<button @click="reset">重试</button>
|
||
</div>
|
||
<slot v-else></slot>
|
||
</template>
|
||
|
||
// 使用
|
||
<ErrorBoundary>
|
||
<RiskyComponent />
|
||
</ErrorBoundary>
|
||
|
||
// 全局错误处理
|
||
app.config.errorHandler = (err, instance, info) => {
|
||
console.error('Global error:', err)
|
||
console.log('Component:', instance)
|
||
console.log('Error info:', info)
|
||
|
||
// 发送到错误监控服务
|
||
Sentry.captureException(err)
|
||
}
|
||
|
||
// 处理异步错误
|
||
app.config.warnHandler = (msg, instance, trace) => {
|
||
console.warn('Warning:', msg)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
Vue 3 面试重点掌握:
|
||
|
||
1. **响应式原理**:Proxy、依赖收集、触发更新的完整流程
|
||
2. **Composition API**:setup、ref/reactive、watch/computed 原理
|
||
3. **编译优化**:静态提升、PatchFlags、Block Tree
|
||
4. **Diff 算法**:快速 Diff、最长递增子序列
|
||
5. **性能优化**:v-memo、shallowRef、组件懒加载
|
||
6. **实战能力**:组合式函数封装、状态管理、错误处理
|
||
|
||
---
|
||
|
||
> 持续更新中,欢迎补充!
|