前端面试高频题
一、Vue 核心原理
1. Vue 3 响应式系统原理
问题:Vue 3 的响应式系统相比 Vue 2 有哪些改进?请详细说明 Proxy 的优势。
回答要点:
Vue 3 使用 Proxy 替代了 Vue 2 的 Object.defineProperty,带来以下核心改进:
完整的对象监听能力
- Proxy 可以拦截对象的所有操作(13种),而 Object.defineProperty 只能监听属性的读写
- 可以监听数组索引和 length 属性的变化
- 可以监听动态添加的属性
性能优化
- 懒监听:只有被访问的属性才会被追踪
- 不需要递归遍历所有属性进行劫持
- 初始化性能更好
代码示例:
// Vue 3 响应式实现核心
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 hadKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
trigger(target, key)
}
return result
}
})
}
// 依赖收集
let activeEffect = null
const targetMap = new WeakMap()
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())
}
}
// 副作用函数
function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}
2. Vue 3 Composition API 设计思想
问题:Composition API 解决了什么问题?它的组合逻辑如何优于 Options API?
核心优势:
逻辑复用更优雅
- 替代 mixins,避免命名冲突
- 清晰的数据来源
- 更好的类型推导
代码组织更灵活
- 按功能组织代码,而非按选项类型
- 相关逻辑集中管理
- 更好的可读性和维护性
实战示例:
// 可复用的组合函数
import { ref, onMounted, onUnmounted } from 'vue'
// 鼠标位置追踪
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 }
}
// 数据请求
function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
async function fetchData() {
loading.value = true
error.value = null
try {
const response = await fetch(url)
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
onMounted(() => {
fetchData()
})
return { data, error, loading, refetch: fetchData }
}
// 组件中使用
export default {
setup() {
const { x, y } = useMouse()
const { data, error, loading } = useFetch('/api/users')
return {
x,
y,
data,
error,
loading
}
}
}
3. Vue 虚拟 DOM 和 Diff 算法
问题:详细描述 Vue 3 的 Diff 算法优化策略。
核心优化:
编译时优化
- 静态提升(Static Hoisting)
- 补丁标记(Patch Flags)
- 树结构打平(Block Tree)
运行时优化
- 最长递增子序列算法(LIS)
- 快速路径判断
Diff 算法实现:
function patchChildren(n1, n2, container) {
const c1 = n1.children
const c2 = n2.children
// 快速路径:文本节点
if (typeof c2 === 'string') {
if (c1 !== c2) {
container.textContent = c2
}
return
}
// 数组 Diff
const oldLength = c1.length
const newLength = c2.length
const commonLength = Math.min(oldLength, newLength)
// 1. 从头部开始对比
let i = 0
while (i < commonLength && c1[i].key === c2[i].key) {
patch(c1[i], c2[i], container)
i++
}
// 2. 从尾部开始对比
let e1 = oldLength - 1
let e2 = newLength - 1
while (e1 >= i && e2 >= i && c1[e1].key === c2[e2].key) {
patch(c1[e1], c2[e2], container)
e1--
e2--
}
// 3. 只有新增节点
if (i > e1 && i <= e2) {
while (i <= e2) {
mount(c2[i], container)
i++
}
}
// 4. 只有删除节点
else if (i > e2 && i <= e1) {
while (i <= e1) {
unmount(c1[i])
i++
}
}
// 5. 乱序情况:最长递增子序列
else {
const s1 = i
const s2 = i
// 构建新节点的 key -> index 映射
const keyToNewIndexMap = new Map()
for (i = s2; i <= e2; i++) {
keyToNewIndexMap.set(c2[i].key, i)
}
// 用于存储新节点在旧节点中的位置
const newIndexToOldIndexMap = new Array(e2 - s2 + 1).fill(-1)
let patched = 0
const toBePatched = e2 - s2 + 1
let moved = false
let maxNewIndexSoFar = 0
// 遍历旧节点
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
if (patched >= toBePatched) {
// 所有新节点都已处理,删除剩余旧节点
unmount(prevChild)
continue
}
const newIndex = keyToNewIndexMap.get(prevChild.key)
if (newIndex === undefined) {
// 旧节点在新节点中不存在,删除
unmount(prevChild)
} else {
// 记录位置关系
newIndexToOldIndexMap[newIndex - s2] = i
// 判断是否需要移动
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
// 更新节点
patch(prevChild, c2[newIndex], container)
patched++
}
}
// 计算最长递增子序列
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: []
let j = increasingNewIndexSequence.length - 1
// 倒序遍历,插入和移动节点
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex]
const anchor = nextIndex + 1 < newLength ? c2[nextIndex + 1].el : null
if (newIndexToOldIndexMap[i] === -1) {
// 新增节点
mount(nextChild, container, anchor)
} else if (moved) {
// 需要移动
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor)
} else {
j--
}
}
}
}
}
// 最长递增子序列算法
function getSequence(arr) {
const len = arr.length
const result = [0]
const p = arr.slice()
let i, j, u, v, c
for (i = 0; i < len; i++) {
const arrI = arr[i]
if (arrI !== -1) {
j = result[result.length - 1]
if (arr[j] < arrI) {
p[i] = j
result.push(i)
continue
}
u = 0
v = result.length - 1
while (u < v) {
c = ((u + v) / 2) | 0
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
result[u] = i
}
}
}
u = result.length
v = result[u - 1]
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}
二、React 核心原理
4. React 18 并发特性
问题:React 18 的并发渲染(Concurrent Rendering)是如何实现的?
核心概念:
时间切片(Time Slicing)
- 将长任务拆分成多个小任务
- 每个任务执行 5ms 后交还控制权
- 使用 MessageChannel 实现调度
优先级调度
- 不同更新具有不同优先级
- 高优先级任务可以打断低优先级任务
- 过期任务会被提升优先级
实现原理:
// Scheduler 核心实现
const ImmediatePriority = 1
const UserBlockingPriority = 2
const NormalPriority = 3
const LowPriority = 4
const IdlePriority = 5
// 任务队列
let taskQueue = []
let timerQueue = []
// 当前任务
let currentTask = null
let isHostCallbackScheduled = false
let isPerformingWork = false
// 调度任务
function scheduleCallback(priorityLevel, callback, options) {
const currentTime = getCurrentTime()
let startTime
if (options && typeof options.delay === 'number') {
startTime = currentTime + options.delay
} else {
startTime = currentTime
}
let timeout
switch (priorityLevel) {
case ImmediatePriority:
timeout = -1
break
case UserBlockingPriority:
timeout = 250
break
case IdlePriority:
timeout = 1073741823
break
case LowPriority:
timeout = 10000
break
case NormalPriority:
default:
timeout = 5000
break
}
const expirationTime = startTime + timeout
const newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1
}
if (startTime > currentTime) {
// 延迟任务
newTask.sortIndex = startTime
push(timerQueue, newTask)
if (!isHostCallbackScheduled && peek(taskQueue) === null) {
requestHostTimeout(handleTimeout, startTime - currentTime)
}
} else {
// 立即执行任务
newTask.sortIndex = expirationTime
push(taskQueue, newTask)
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true
requestHostCallback(flushWork)
}
}
return newTask
}
// 执行任务
function flushWork(hasTimeRemaining, initialTime) {
isHostCallbackScheduled = false
isPerformingWork = true
try {
return workLoop(hasTimeRemaining, initialTime)
} finally {
currentTask = null
isPerformingWork = false
}
}
// 工作循环
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime
// 检查延迟任务
advanceTimers(currentTime)
currentTask = peek(taskQueue)
while (currentTask !== null) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// 时间片用完,让出控制权
break
}
const callback = currentTask.callback
if (typeof callback === 'function') {
currentTask.callback = null
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime
const continuationCallback = callback(didUserCallbackTimeout)
currentTime = getCurrentTime()
if (typeof continuationCallback === 'function') {
// 任务未完成,继续执行
currentTask.callback = continuationCallback
} else {
// 任务完成,移除
if (currentTask === peek(taskQueue)) {
pop(taskQueue)
}
}
advanceTimers(currentTime)
} else {
pop(taskQueue)
}
currentTask = peek(taskQueue)
}
// 判断是否还有任务
if (currentTask !== null) {
return true
} else {
const firstTimer = peek(timerQueue)
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime)
}
return false
}
}
// 使用 MessageChannel 实现调度
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime()
const hasTimeRemaining = frameDeadline > currentTime
try {
const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime)
if (!hasMoreWork) {
scheduledHostCallback = null
} else {
// 还有工作,继续调度
port.postMessage(null)
}
} catch (error) {
// 发生错误,重新调度
port.postMessage(null)
throw error
}
}
}
function requestHostCallback(callback) {
scheduledHostCallback = callback
port.postMessage(null)
}
// 判断是否应该让出控制权
function shouldYieldToHost() {
const currentTime = getCurrentTime()
return currentTime >= frameDeadline
}
5. React Fiber 架构
问题:详细说明 Fiber 架构的工作原理和双缓存机制。
Fiber 节点结构:
function FiberNode(tag, pendingProps, key, mode) {
// 实例属性
this.tag = tag
this.key = key
this.elementType = null
this.type = null
this.stateNode = null
// Fiber 链表结构
this.return = null // 父节点
this.child = null // 第一个子节点
this.sibling = null // 下一个兄弟节点
this.index = 0
this.ref = null
// 工作单元属性
this.pendingProps = pendingProps
this.memoizedProps = null
this.updateQueue = null
this.memoizedState = null
this.dependencies = null
this.mode = mode
// Effects
this.flags = NoFlags
this.subtreeFlags = NoFlags
this.deletions = null
// 优先级
this.lanes = NoLanes
this.childLanes = NoLanes
// 双缓存
this.alternate = null
}
// 工作循环
function workLoopConcurrent() {
// 当有工作且未超时时继续执行
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress)
}
}
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate
// 开始工作
let next = beginWork(current, unitOfWork, renderLanes)
unitOfWork.memoizedProps = unitOfWork.pendingProps
if (next === null) {
// 没有子节点,完成工作
completeUnitOfWork(unitOfWork)
} else {
// 继续处理子节点
workInProgress = next
}
}
// 开始阶段
function beginWork(current, workInProgress, renderLanes) {
if (current !== null) {
const oldProps = current.memoizedProps
const newProps = workInProgress.pendingProps
if (
oldProps !== newProps ||
hasLegacyContextChanged()
) {
didReceiveUpdate = true
} else {
// 可以复用
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes
)
if (!hasScheduledUpdateOrContext) {
didReceiveUpdate = false
return attemptEarlyBailout(current, workInProgress, renderLanes)
}
didReceiveUpdate = false
}
} else {
didReceiveUpdate = false
}
// 清除优先级
workInProgress.lanes = NoLanes
// 根据不同类型处理
switch (workInProgress.tag) {
case FunctionComponent: {
const Component = workInProgress.type
const unresolvedProps = workInProgress.pendingProps
return updateFunctionComponent(
current,
workInProgress,
Component,
unresolvedProps,
renderLanes
)
}
case ClassComponent: {
const Component = workInProgress.type
const unresolvedProps = workInProgress.pendingProps
return updateClassComponent(
current,
workInProgress,
Component,
unresolvedProps,
renderLanes
)
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes)
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes)
// ... 其他类型
}
}
// 完成阶段
function completeUnitOfWork(unitOfWork) {
let completedWork = unitOfWork
do {
const current = completedWork.alternate
const returnFiber = completedWork.return
// 完成工作
const next = completeWork(current, completedWork, renderLanes)
if (next !== null) {
// 发现新工作
workInProgress = next
return
}
// 收集副作用
if (returnFiber !== null) {
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect
}
returnFiber.lastEffect = completedWork.lastEffect
}
const flags = completedWork.flags
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork
} else {
returnFiber.firstEffect = completedWork
}
returnFiber.lastEffect = completedWork
}
}
const siblingFiber = completedWork.sibling
if (siblingFiber !== null) {
// 处理兄弟节点
workInProgress = siblingFiber
return
}
// 返回父节点
completedWork = returnFiber
workInProgress = completedWork
} while (completedWork !== null)
}
6. React Hooks 原理
问题:useState 和 useEffect 的实现原理是什么?
Hooks 实现:
// Hook 数据结构
const hook = {
memoizedState: null, // 当前状态
baseState: null, // 基础状态
baseQueue: null, // 基础更新队列
queue: null, // 更新队列
next: null // 下一个 Hook
}
// 当前正在渲染的 Fiber
let currentlyRenderingFiber = null
// 当前工作中的 Hook
let workInProgressHook = null
// 当前 Hook 对应的旧 Hook
let currentHook = null
// useState 实现
function useState(initialState) {
return useReducer(
basicStateReducer,
initialState
)
}
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action
}
function useReducer(reducer, initialArg, init) {
const hook = mountWorkInProgressHook()
let initialState
if (init !== undefined) {
initialState = init(initialArg)
} else {
initialState = initialArg
}
hook.memoizedState = hook.baseState = initialState
const queue = {
pending: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: initialState
}
hook.queue = queue
const dispatch = (queue.dispatch = dispatchAction.bind(
null,
currentlyRenderingFiber,
queue
))
return [hook.memoizedState, dispatch]
}
// 更新状态
function dispatchAction(fiber, queue, action) {
const update = {
action,
next: null
}
// 构建环形链表
const pending = queue.pending
if (pending === null) {
update.next = update
} else {
update.next = pending.next
pending.next = update
}
queue.pending = update
// 调度更新
const alternate = fiber.alternate
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// 渲染期间的更新
didScheduleRenderPhaseUpdateDuringThisPass = true
} else {
// 立即计算新状态(性能优化)
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer
if (lastRenderedReducer !== null) {
try {
const currentState = queue.lastRenderedState
const eagerState = lastRenderedReducer(currentState, action)
if (Object.is(eagerState, currentState)) {
// 状态未变化,跳过更新
return
}
} catch (error) {
// 忽略错误
}
}
}
scheduleUpdateOnFiber(fiber, lane, eventTime)
}
}
// useEffect 实现
function useEffect(create, deps) {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps
)
}
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
const hook = mountWorkInProgressHook()
const nextDeps = deps === undefined ? null : deps
currentlyRenderingFiber.flags |= fiberFlags
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps
)
}
function pushEffect(tag, create, destroy, deps) {
const effect = {
tag,
create,
destroy,
deps,
next: null
}
let componentUpdateQueue = currentlyRenderingFiber.updateQueue
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue()
currentlyRenderingFiber.updateQueue = componentUpdateQueue
componentUpdateQueue.lastEffect = effect.next = effect
} else {
const lastEffect = componentUpdateQueue.lastEffect
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect
} else {
const firstEffect = lastEffect.next
lastEffect.next = effect
effect.next = firstEffect
componentUpdateQueue.lastEffect = effect
}
}
return effect
}
// 更新时的 useEffect
function updateEffect(create, deps) {
return updateEffectImpl(
PassiveEffect,
HookPassive,
create,
deps
)
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
const hook = updateWorkInProgressHook()
const nextDeps = deps === undefined ? null : deps
let destroy = undefined
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState
destroy = prevEffect.destroy
if (nextDeps !== null) {
const prevDeps = prevEffect.deps
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 依赖未变化,不执行
pushEffect(hookFlags, create, destroy, nextDeps)
return
}
}
}
currentlyRenderingFiber.flags |= fiberFlags
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps
)
}
// 执行 Effects
function commitHookEffectListMount(tag, finishedWork) {
const updateQueue = finishedWork.updateQueue
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null
if (lastEffect !== null) {
const firstEffect = lastEffect.next
let effect = firstEffect
do {
if ((effect.tag & tag) === tag) {
const create = effect.create
effect.destroy = create()
}
effect = effect.next
} while (effect !== firstEffect)
}
}
function commitHookEffectListUnmount(tag, finishedWork) {
const updateQueue = finishedWork.updateQueue
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null
if (lastEffect !== null) {
const firstEffect = lastEffect.next
let effect = firstEffect
do {
if ((effect.tag & tag) === tag) {
const destroy = effect.destroy
effect.destroy = undefined
if (destroy !== undefined) {
destroy()
}
}
effect = effect.next
} while (effect !== firstEffect)
}
}
三、TypeScript 高级特性
7. TypeScript 类型系统
问题:如何实现高级类型推导和类型体操?
高级类型示例:
// 1. 深度只读
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P]
: DeepReadonly<T[P]>
: T[P]
}
// 2. 深度可选
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? T[P] extends Function
? T[P]
: DeepPartial<T[P]>
: T[P]
}
// 3. 类型过滤
type Filter<T, U> = T extends U ? T : never
type Example1 = Filter<'a' | 'b' | 'c' | 1 | 2, string> // 'a' | 'b' | 'c'
// 4. 获取函数返回类型
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any
// 5. 获取 Promise 的值类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T
type Example2 = UnwrapPromise<Promise<string>> // string
// 6. 元组转联合类型
type TupleToUnion<T extends any[]> = T[number]
type Example3 = TupleToUnion<[string, number, boolean]> // string | number | boolean
// 7. 联合类型转交叉类型
type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never
type Example4 = UnionToIntersection<{ a: string } | { b: number }>
// { a: string } & { b: number }
// 8. 必选属性
type Required<T> = {
[P in keyof T]-?: T[P]
}
// 9. 挑选属性
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
// 10. 排除属性
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
// 11. 函数参数类型
type Parameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never
// 12. 构造函数参数类型
type ConstructorParameters<T extends new (...args: any) => any> =
T extends new (...args: infer P) => any ? P : never
// 13. 递归对象路径
type PathImpl<T, K extends keyof T> = K extends string
? T[K] extends Record<string, any>
? T[K] extends ArrayLike<any>
? K | `${K}.${PathImpl<T[K], Exclude<keyof T[K], keyof any[]>>}`
: K | `${K}.${PathImpl<T[K], keyof T[K]>}`
: K
: never
type Path<T> = PathImpl<T, keyof T> | keyof T
// 使用示例
interface User {
name: string
age: number
address: {
city: string
street: {
name: string
number: number
}
}
hobbies: string[]
}
type UserPath = Path<User>
// 'name' | 'age' | 'address' | 'address.city' | 'address.street' |
// 'address.street.name' | 'address.street.number' | 'hobbies'
// 14. 根据路径获取类型
type PathValue<T, P extends string> = P extends keyof T
? T[P]
: P extends `${infer K}.${infer Rest}`
? K extends keyof T
? PathValue<T[K], Rest>
: never
: never
type CityType = PathValue<User, 'address.city'> // string
type StreetNumberType = PathValue<User, 'address.street.number'> // number
8. TypeScript 装饰器
问题:实现常用的装饰器模式。
装饰器实现:
// 1. 方法装饰器 - 日志记录
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with args:`, args)
const result = originalMethod.apply(this, args)
console.log(`Result:`, result)
return result
}
return descriptor
}
// 2. 方法装饰器 - 性能测量
function Measure(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const start = performance.now()
const result = await originalMethod.apply(this, args)
const end = performance.now()
console.log(`${propertyKey} took ${end - start}ms`)
return result
}
return descriptor
}
// 3. 方法装饰器 - 缓存
function Memoize(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value
const cache = new Map<string, any>()
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args)
if (cache.has(key)) {
return cache.get(key)
}
const result = originalMethod.apply(this, args)
cache.set(key, result)
return result
}
return descriptor
}
// 4. 方法装饰器 - 防抖
function Debounce(delay: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value
let timeoutId: NodeJS.Timeout
descriptor.value = function (...args: any[]) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
originalMethod.apply(this, args)
}, delay)
}
return descriptor
}
}
// 5. 方法装饰器 - 节流
function Throttle(delay: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value
let lastTime = 0
descriptor.value = function (...args: any[]) {
const now = Date.now()
if (now - lastTime >= delay) {
lastTime = now
return originalMethod.apply(this, args)
}
}
return descriptor
}
}
// 6. 属性装饰器 - 验证
function MinLength(length: number) {
return function (target: any, propertyKey: string) {
let value: string
const getter = function () {
return value
}
const setter = function (newVal: string) {
if (newVal.length < length) {
throw new Error(`${propertyKey} must be at least ${length} characters`)
}
value = newVal
}
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
})
}
}
// 7. 类装饰器 - 单例模式
function Singleton<T extends { new (...args: any[]): {} }>(constructor: T) {
let instance: T | null = null
return new Proxy(constructor, {
construct(target, args) {
if (!instance) {
instance = new target(...args)
}
return instance
}
})
}
// 8. 参数装饰器 - 验证
const requiredMetadataKey = Symbol('required')
function Required(
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) {
let existingRequiredParameters: number[] =
Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || []
existingRequiredParameters.push(parameterIndex)
Reflect.defineMetadata(
requiredMetadataKey,
existingRequiredParameters,
target,
propertyKey
)
}
function Validate(
target: any,
propertyName: string,
descriptor: PropertyDescriptor
) {
const method = descriptor.value
descriptor.value = function (...args: any[]) {
const requiredParameters: number[] = Reflect.getOwnMetadata(
requiredMetadataKey,
target,
propertyName
)
if (requiredParameters) {
for (const parameterIndex of requiredParameters) {
if (
parameterIndex >= args.length ||
args[parameterIndex] === undefined ||
args[parameterIndex] === null
) {
throw new Error(`Missing required argument at position ${parameterIndex}`)
}
}
}
return method.apply(this, args)
}
}
// 使用示例
class UserService {
@Log
@Measure
async getUser(id: number) {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 100))
return { id, name: 'User ' + id }
}
@Memoize
calculateExpensiveValue(n: number): number {
console.log('Calculating...')
return n * n
}
@Debounce(300)
handleSearch(query: string) {
console.log('Searching for:', query)
}
@Throttle(1000)
handleScroll() {
console.log('Scrolling...')
}
@Validate
createUser(@Required name: string, @Required email: string, age?: number) {
console.log('Creating user:', { name, email, age })
}
}
@Singleton
class Database {
constructor() {
console.log('Database instance created')
}
}
class User {
@MinLength(3)
username: string
constructor(username: string) {
this.username = username
}
}
四、浏览器原理
9. 浏览器渲染流程
问题:详细描述浏览器从 HTML 到像素的完整渲染流程。
渲染流程:
- 解析 HTML → DOM 树
- 解析 CSS → CSSOM 树
- 合并 → 渲染树(Render Tree)
- 布局(Layout/Reflow)
- 绘制(Paint)
- 合成(Composite)
关键优化点:
// 1. 减少重排(Reflow)
// 不好的做法
const el = document.getElementById('element')
el.style.width = '100px'
el.style.height = '100px'
el.style.margin = '10px'
// 触发 3 次重排
// 好的做法
el.style.cssText = 'width: 100px; height: 100px; margin: 10px;'
// 只触发 1 次重排
// 使用 class
el.className = 'new-class'
// 2. 批量 DOM 操作
// 不好的做法
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li')
li.textContent = i
document.getElementById('list').appendChild(li)
}
// 触发 1000 次重排
// 好的做法:使用 DocumentFragment
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li')
li.textContent = i
fragment.appendChild(li)
}
document.getElementById('list').appendChild(fragment)
// 只触发 1 次重排
// 3. 读写分离
// 不好的做法
const width1 = div1.clientWidth // 读
div1.style.width = width1 + 10 + 'px' // 写
const width2 = div2.clientWidth // 读(触发强制重排)
div2.style.width = width2 + 10 + 'px' // 写
// 好的做法:先读后写
const width1 = div1.clientWidth
const width2 = div2.clientWidth
div1.style.width = width1 + 10 + 'px'
div2.style.width = width2 + 10 + 'px'
// 4. 使用 transform 代替 top/left
// 不好的做法(触发重排)
element.style.left = element.offsetLeft + 10 + 'px'
// 好的做法(只触发合成)
element.style.transform = 'translateX(10px)'
// 5. 使用 will-change 提示浏览器
.moving-element {
will-change: transform;
}
// 6. 使用 requestAnimationFrame
function animate() {
element.style.transform = `translateX(${x}px)`
x += 1
if (x < 100) {
requestAnimationFrame(animate)
}
}
requestAnimationFrame(animate)
10. 事件循环机制
问题:详细说明浏览器和 Node.js 的事件循环差异。
浏览器事件循环:
// 事件循环执行顺序
// 1. 执行同步代码
// 2. 执行微任务队列
// 3. 执行一个宏任务
// 4. 执行微任务队列
// 5. 渲染(如果需要)
// 6. 回到步骤 3
console.log('1')
setTimeout(() => {
console.log('2')
Promise.resolve().then(() => {
console.log('3')
})
}, 0)
new Promise((resolve) => {
console.log('4')
resolve()
}).then(() => {
console.log('5')
}).then(() => {
console.log('6')
})
console.log('7')
// 输出顺序:1, 4, 7, 5, 6, 2, 3
// 微任务:
// - Promise.then/catch/finally
// - MutationObserver
// - queueMicrotask
// - process.nextTick (Node.js)
// 宏任务:
// - setTimeout
// - setInterval
// - setImmediate (Node.js)
// - I/O
// - UI rendering
// - requestAnimationFrame
// 复杂示例
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
console.log('promise1')
resolve()
}).then(() => {
console.log('promise2')
})
console.log('script end')
// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
Node.js 事件循环:
// Node.js 事件循环阶段
// 1. timers: setTimeout/setInterval
// 2. pending callbacks: 系统级回调
// 3. idle, prepare: 内部使用
// 4. poll: I/O 回调
// 5. check: setImmediate
// 6. close callbacks: 关闭回调
// process.nextTick 优先级高于微任务
console.log('start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
process.nextTick(() => {
console.log('nextTick')
})
Promise.resolve().then(() => {
console.log('promise')
})
console.log('end')
// 输出顺序:
// start
// end
// nextTick
// promise
// setTimeout
// setImmediate
// 注意:setTimeout 和 setImmediate 的顺序可能变化
// 在 I/O 回调中,setImmediate 总是先于 setTimeout
11. 浏览器缓存策略
问题:详细说明强缓存和协商缓存的工作原理。
缓存策略:
// 1. 强缓存
// Cache-Control (HTTP/1.1,优先级高)
Cache-Control: max-age=3600 // 缓存 1 小时
Cache-Control: no-cache // 每次使用前验证
Cache-Control: no-store // 不缓存
Cache-Control: public // 可被任何缓存存储
Cache-Control: private // 只能被浏览器缓存
// Expires (HTTP/1.0)
Expires: Wed, 21 Oct 2025 07:28:00 GMT
// 2. 协商缓存
// Last-Modified / If-Modified-Since
// 服务器响应
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
// 客户端请求
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
// ETag / If-None-Match (优先级高)
// 服务器响应
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
// 客户端请求
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
// 3. 缓存决策树实现
class CacheStrategy {
constructor() {
this.cache = new Map()
}
// 设置缓存
set(key, value, options = {}) {
const {
maxAge = 3600, // 默认 1 小时
staleWhileRevalidate = 0,
mustRevalidate = false
} = options
const item = {
value,
timestamp: Date.now(),
maxAge,
staleWhileRevalidate,
mustRevalidate,
etag: this.generateETag(value)
}
this.cache.set(key, item)
}
// 获取缓存
async get(key, fetchFn) {
const item = this.cache.get(key)
if (!item) {
// 缓存不存在,获取新数据
const value = await fetchFn()
this.set(key, value)
return { value, fromCache: false }
}
const age = Date.now() - item.timestamp
// 强缓存未过期
if (age < item.maxAge * 1000) {
return { value: item.value, fromCache: true, fresh: true }
}
// 在 staleWhileRevalidate 期间
if (age < (item.maxAge + item.staleWhileRevalidate) * 1000) {
// 返回旧数据,后台刷新
this.revalidateInBackground(key, fetchFn)
return { value: item.value, fromCache: true, fresh: false }
}
// 缓存过期,需要重新验证
if (item.mustRevalidate) {
const isModified = await this.checkModified(key, item.etag)
if (!isModified) {
// 304 Not Modified
item.timestamp = Date.now()
return { value: item.value, fromCache: true, revalidated: true }
}
}
// 获取新数据
const value = await fetchFn()
this.set(key, value)
return { value, fromCache: false }
}
// 生成 ETag
generateETag(value) {
const str = JSON.stringify(value)
let hash = 0
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash
}
return hash.toString(36)
}
// 后台重新验证
async revalidateInBackground(key, fetchFn) {
try {
const value = await fetchFn()
this.set(key, value)
} catch (error) {
console.error('Background revalidation failed:', error)
}
}
// 检查是否修改
async checkModified(key, etag) {
// 实际应该向服务器发送请求
// 这里简化处理
return Math.random() > 0.5
}
}
// 使用示例
const cache = new CacheStrategy()
async function fetchUserData(id) {
return cache.get(`user:${id}`, async () => {
const response = await fetch(`/api/users/${id}`)
return response.json()
})
}
// 4. Service Worker 缓存策略
// Cache First (缓存优先)
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request)
})
)
})
// Network First (网络优先)
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.then(response => {
const responseClone = response.clone()
caches.open('v1').then(cache => {
cache.put(event.request, responseClone)
})
return response
})
.catch(() => {
return caches.match(event.request)
})
)
})
// Stale While Revalidate (返回缓存,后台更新)
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('v1').then(cache => {
return cache.match(event.request).then(response => {
const fetchPromise = fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone())
return networkResponse
})
return response || fetchPromise
})
})
)
})
五、性能优化
12. 首屏加载优化
问题:如何优化首屏加载时间?提供具体方案。
优化方案:
// 1. 代码分割 (Code Splitting)
// 路由懒加载
const Home = () => import('./views/Home.vue')
const About = () => import('./views/About.vue')
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
// React 代码分割
import { lazy, Suspense } from 'react'
const LazyComponent = lazy(() => import('./LazyComponent'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
)
}
// 2. 预加载 (Preload/Prefetch)
// HTML
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="critical.js" as="script">
<link rel="prefetch" href="next-page.js">
// JavaScript
const link = document.createElement('link')
link.rel = 'prefetch'
link.href = '/next-page.js'
document.head.appendChild(link)
// 3. 资源压缩
// Webpack 配置
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}),
new CssMinimizerPlugin()
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
}
// 4. 图片优化
// 响应式图片
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.jpg" type="image/jpeg">
<img src="image.jpg" alt="Description">
</picture>
// 懒加载图片
class LazyImageLoader {
constructor(selector = '.lazy-image') {
this.images = document.querySelectorAll(selector)
this.observer = null
this.init()
}
init() {
if ('IntersectionObserver' in window) {
this.observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target)
this.observer.unobserve(entry.target)
}
})
},
{
rootMargin: '50px'
}
)
this.images.forEach(img => this.observer.observe(img))
} else {
// 降级方案
this.images.forEach(img => this.loadImage(img))
}
}
loadImage(img) {
const src = img.dataset.src
const srcset = img.dataset.srcset
if (src) {
img.src = src
}
if (srcset) {
img.srcset = srcset
}
img.classList.add('loaded')
}
}
// 使用
new LazyImageLoader()
// 5. 关键 CSS 内联
// 提取关键 CSS
const critical = require('critical')
critical.generate({
inline: true,
base: 'dist/',
src: 'index.html',
target: {
html: 'index-critical.html'
},
width: 1300,
height: 900
})
// 6. DNS 预解析
<link rel="dns-prefetch" href="//cdn.example.com">
<link rel="preconnect" href="//api.example.com">
// 7. HTTP/2 Server Push
// 服务器配置
Link: </styles.css>; rel=preload; as=style
Link: </script.js>; rel=preload; as=script
// 8. 骨架屏
const SkeletonScreen = () => {
return (
<div className="skeleton">
<div className="skeleton-header"></div>
<div className="skeleton-content">
<div className="skeleton-line"></div>
<div className="skeleton-line"></div>
<div className="skeleton-line"></div>
</div>
</div>
)
}
// CSS
.skeleton-line {
height: 16px;
margin: 10px 0;
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
// 9. SSR/SSG
// Next.js SSG
export async function getStaticProps() {
const data = await fetchData()
return {
props: { data },
revalidate: 60 // ISR: 每 60 秒重新生成
}
}
// Next.js SSR
export async function getServerSideProps(context) {
const data = await fetchData()
return {
props: { data }
}
}
// 10. 性能监控
class PerformanceMonitor {
constructor() {
this.metrics = {}
this.observe()
}
observe() {
// 页面加载性能
if ('PerformanceObserver' in window) {
// FCP (First Contentful Paint)
new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
this.metrics.fcp = entry.startTime
this.report('fcp', entry.startTime)
}
}
}).observe({ entryTypes: ['paint'] })
// LCP (Largest Contentful Paint)
new PerformanceObserver(list => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
this.metrics.lcp = lastEntry.renderTime || lastEntry.loadTime
this.report('lcp', this.metrics.lcp)
}).observe({ entryTypes: ['largest-contentful-paint'] })
// FID (First Input Delay)
new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
this.metrics.fid = entry.processingStart - entry.startTime
this.report('fid', this.metrics.fid)
}
}).observe({ entryTypes: ['first-input'] })
// CLS (Cumulative Layout Shift)
let clsScore = 0
new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value
this.metrics.cls = clsScore
this.report('cls', clsScore)
}
}
}).observe({ entryTypes: ['layout-shift'] })
}
// 传统性能指标
window.addEventListener('load', () => {
const timing = performance.timing
this.metrics.dns = timing.domainLookupEnd - timing.domainLookupStart
this.metrics.tcp = timing.connectEnd - timing.connectStart
this.metrics.ttfb = timing.responseStart - timing.requestStart
this.metrics.download = timing.responseEnd - timing.responseStart
this.metrics.domParse = timing.domInteractive - timing.domLoading
this.metrics.domReady = timing.domContentLoadedEventEnd - timing.navigationStart
this.metrics.load = timing.loadEventEnd - timing.navigationStart
this.reportAll()
})
}
report(metric, value) {
// 发送到分析服务
console.log(`${metric}: ${value}ms`)
// 实际应该发送到后端
// navigator.sendBeacon('/analytics', JSON.stringify({ metric, value }))
}
reportAll() {
console.table(this.metrics)
}
}
// 使用
new PerformanceMonitor()
13. 长列表优化
问题:如何优化包含万条数据的列表渲染?
虚拟滚动实现:
// 虚拟滚动组件
class VirtualScroller {
constructor(options) {
this.container = options.container
this.items = options.items
this.itemHeight = options.itemHeight
this.renderItem = options.renderItem
this.buffer = options.buffer || 3
this.scrollTop = 0
this.containerHeight = this.container.clientHeight
this.init()
}
init() {
// 创建容器
this.phantom = document.createElement('div')
this.phantom.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
height: ${this.items.length * this.itemHeight}px;
z-index: -1;
`
this.content = document.createElement('div')
this.content.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
`
this.container.style.cssText = `
position: relative;
overflow-y: auto;
`
this.container.appendChild(this.phantom)
this.container.appendChild(this.content)
// 监听滚动
this.container.addEventListener('scroll', () => {
this.scrollTop = this.container.scrollTop
this.render()
})
// 初始渲染
this.render()
}
render() {
// 计算可见范围
const start = Math.floor(this.scrollTop / this.itemHeight)
const end = Math.ceil((this.scrollTop + this.containerHeight) / this.itemHeight)
// 添加缓冲区
const visibleStart = Math.max(0, start - this.buffer)
const visibleEnd = Math.min(this.items.length, end + this.buffer)
// 计算偏移
const offsetY = visibleStart * this.itemHeight
// 渲染可见项
const fragment = document.createDocumentFragment()
for (let i = visibleStart; i < visibleEnd; i++) {
const item = this.renderItem(this.items[i], i)
item.style.cssText = `
position: absolute;
top: ${i * this.itemHeight}px;
left: 0;
right: 0;
height: ${this.itemHeight}px;
`
fragment.appendChild(item)
}
this.content.innerHTML = ''
this.content.appendChild(fragment)
}
// 更新数据
update(items) {
this.items = items
this.phantom.style.height = `${items.length * this.itemHeight}px`
this.render()
}
// 滚动到指定项
scrollToIndex(index) {
this.container.scrollTop = index * this.itemHeight
}
}
// 使用示例
const container = document.getElementById('list-container')
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i}`
}))
const virtualScroller = new VirtualScroller({
container,
items,
itemHeight: 50,
renderItem: (item, index) => {
const div = document.createElement('div')
div.className = 'list-item'
div.textContent = item.text
div.dataset.index = index
return div
}
})
// React 虚拟滚动组件
import { useState, useRef, useEffect } from 'react'
function VirtualList({ items, itemHeight, height, renderItem }) {
const [scrollTop, setScrollTop] = useState(0)
const containerRef = useRef(null)
const buffer = 3
const start = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer)
const end = Math.min(
items.length,
Math.ceil((scrollTop + height) / itemHeight) + buffer
)
const visibleItems = items.slice(start, end)
const offsetY = start * itemHeight
const handleScroll = (e) => {
setScrollTop(e.target.scrollTop)
}
return (
<div
ref={containerRef}
style={{
height,
overflow: 'auto',
position: 'relative'
}}
onScroll={handleScroll}
>
<div
style={{
height: items.length * itemHeight,
position: 'relative'
}}
>
<div
style={{
transform: `translateY(${offsetY}px)`
}}
>
{visibleItems.map((item, index) =>
renderItem(item, start + index)
)}
</div>
</div>
</div>
)
}
// 使用
function App() {
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i}`
}))
return (
<VirtualList
items={items}
itemHeight={50}
height={600}
renderItem={(item, index) => (
<div
key={item.id}
style={{
height: 50,
borderBottom: '1px solid #eee'
}}
>
{item.text}
</div>
)}
/>
)
}
// Vue 虚拟滚动组件
import { ref, computed, onMounted } from 'vue'
export default {
props: {
items: Array,
itemHeight: Number,
height: Number
},
setup(props, { slots }) {
const scrollTop = ref(0)
const containerRef = ref(null)
const buffer = 3
const visibleRange = computed(() => {
const start = Math.max(
0,
Math.floor(scrollTop.value / props.itemHeight) - buffer
)
const end = Math.min(
props.items.length,
Math.ceil((scrollTop.value + props.height) / props.itemHeight) + buffer
)
return { start, end }
})
const visibleItems = computed(() => {
const { start, end } = visibleRange.value
return props.items.slice(start, end).map((item, index) => ({
item,
index: start + index
}))
})
const offsetY = computed(() => {
return visibleRange.value.start * props.itemHeight
})
const handleScroll = (e) => {
scrollTop.value = e.target.scrollTop
}
return {
containerRef,
visibleItems,
offsetY,
handleScroll
}
},
template: `
<div
ref="containerRef"
:style="{
height: height + 'px',
overflow: 'auto',
position: 'relative'
}"
@scroll="handleScroll"
>
<div :style="{ height: items.length * itemHeight + 'px' }">
<div :style="{ transform: 'translateY(' + offsetY + 'px)' }">
<div
v-for="{ item, index } in visibleItems"
:key="index"
:style="{ height: itemHeight + 'px' }"
>
<slot :item="item" :index="index"></slot>
</div>
</div>
</div>
</div>
`
}
六、网络优化
14. HTTP 优化策略
问题:如何优化 HTTP 请求性能?
优化方案:
// 1. 请求合并
class RequestBatcher {
constructor(batchFn, delay = 10) {
this.batchFn = batchFn
this.delay = delay
this.queue = []
this.timer = null
}
add(request) {
return new Promise((resolve, reject) => {
this.queue.push({ request, resolve, reject })
if (!this.timer) {
this.timer = setTimeout(() => {
this.flush()
}, this.delay)
}
})
}
async flush() {
if (this.queue.length === 0) return
const batch = this.queue.splice(0)
this.timer = null
try {
const requests = batch.map(item => item.request)
const results = await this.batchFn(requests)
batch.forEach((item, index) => {
item.resolve(results[index])
})
} catch (error) {
batch.forEach(item => {
item.reject(error)
})
}
}
}
// 使用示例
const userBatcher = new RequestBatcher(async (userIds) => {
const response = await fetch('/api/users/batch', {
method: 'POST',
body: JSON.stringify({ ids: userIds })
})
return response.json()
})
// 多个请求会被合并
const user1 = await userBatcher.add(1)
const user2 = await userBatcher.add(2)
const user3 = await userBatcher.add(3)
// 实际只发送一个请求: POST /api/users/batch { ids: [1, 2, 3] }
// 2. 请求去重
class RequestDeduplicator {
constructor() {
this.pending = new Map()
}
async request(key, fetchFn) {
// 如果已有相同请求正在进行,返回该请求的 Promise
if (this.pending.has(key)) {
return this.pending.get(key)
}
const promise = fetchFn()
.finally(() => {
this.pending.delete(key)
})
this.pending.set(key, promise)
return promise
}
}
// 使用
const deduplicator = new RequestDeduplicator()
// 多个组件同时请求相同数据,只会发送一次请求
const data1 = await deduplicator.request('user:1', () =>
fetch('/api/users/1').then(r => r.json())
)
const data2 = await deduplicator.request('user:1', () =>
fetch('/api/users/1').then(r => r.json())
)
// data1 和 data2 是同一个 Promise
// 3. 请求重试
async function fetchWithRetry(url, options = {}, retries = 3) {
const {
retryDelay = 1000,
retryOn = [500, 502, 503, 504],
...fetchOptions
} = options
for (let i = 0; i <= retries; i++) {
try {
const response = await fetch(url, fetchOptions)
if (response.ok || !retryOn.includes(response.status)) {
return response
}
if (i < retries) {
await new Promise(resolve =>
setTimeout(resolve, retryDelay * Math.pow(2, i))
)
}
} catch (error) {
if (i === retries) {
throw error
}
await new Promise(resolve =>
setTimeout(resolve, retryDelay * Math.pow(2, i))
)
}
}
}
// 使用
const response = await fetchWithRetry('/api/data', {
method: 'GET',
retries: 3,
retryDelay: 1000,
retryOn: [500, 502, 503, 504]
})
// 4. 请求取消
class CancellableRequest {
constructor() {
this.pendingRequests = new Map()
}
async fetch(key, url, options = {}) {
// 取消之前的请求
this.cancel(key)
const controller = new AbortController()
this.pendingRequests.set(key, controller)
try {
const response = await fetch(url, {
...options,
signal: controller.signal
})
this.pendingRequests.delete(key)
return response
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request cancelled:', key)
}
throw error
}
}
cancel(key) {
const controller = this.pendingRequests.get(key)
if (controller) {
controller.abort()
this.pendingRequests.delete(key)
}
}
cancelAll() {
this.pendingRequests.forEach(controller => controller.abort())
this.pendingRequests.clear()
}
}
// 使用
const cancellableRequest = new CancellableRequest()
// 搜索场景:用户快速输入时取消之前的请求
function handleSearch(query) {
cancellableRequest.fetch('search', `/api/search?q=${query}`)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name !== 'AbortError') {
console.error(error)
}
})
}
// 5. 并发控制
class ConcurrencyController {
constructor(limit = 6) {
this.limit = limit
this.running = 0
this.queue = []
}
async add(fn) {
if (this.running >= this.limit) {
await new Promise(resolve => this.queue.push(resolve))
}
this.running++
try {
return await fn()
} finally {
this.running--
const resolve = this.queue.shift()
if (resolve) {
resolve()
}
}
}
}
// 使用
const controller = new ConcurrencyController(3)
const urls = Array.from({ length: 20 }, (_, i) => `/api/data/${i}`)
const promises = urls.map(url =>
controller.add(() => fetch(url).then(r => r.json()))
)
const results = await Promise.all(promises)
// 6. 超时控制
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController()
const id = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
...options,
signal: controller.signal
})
clearTimeout(id)
return response
} catch (error) {
clearTimeout(id)
if (error.name === 'AbortError') {
throw new Error('Request timeout')
}
throw error
}
}
// 使用
try {
const response = await fetchWithTimeout('/api/data', {}, 3000)
const data = await response.json()
} catch (error) {
console.error(error.message)
}
15. WebSocket 优化
问题:如何实现可靠的 WebSocket 连接和断线重连?
WebSocket 封装:
class ReliableWebSocket {
constructor(url, options = {}) {
this.url = url
this.options = {
reconnectInterval: 1000,
maxReconnectInterval: 30000,
reconnectDecay: 1.5,
maxReconnectAttempts: null,
heartbeatInterval: 30000,
heartbeatMessage: 'ping',
...options
}
this.ws = null
this.reconnectAttempts = 0
this.reconnectTimer = null
this.heartbeatTimer = null
this.forcedClose = false
this.listeners = new Map()
this.connect()
}
connect() {
try {
this.ws = new WebSocket(this.url)
this.ws.onopen = (event) => {
console.log('WebSocket connected')
this.reconnectAttempts = 0
this.startHeartbeat()
this.emit('open', event)
}
this.ws.onmessage = (event) => {
// 重置心跳
this.resetHeartbeat()
try {
const data = JSON.parse(event.data)
this.emit('message', data)
} catch (error) {
this.emit('message', event.data)
}
}
this.ws.onerror = (event) => {
console.error('WebSocket error:', event)
this.emit('error', event)
}
this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason)
this.stopHeartbeat()
this.emit('close', event)
if (!this.forcedClose) {
this.reconnect()
}
}
} catch (error) {
console.error('WebSocket connection failed:', error)
this.reconnect()
}
}
reconnect() {
if (
this.options.maxReconnectAttempts &&
this.reconnectAttempts >= this.options.maxReconnectAttempts
) {
console.error('Max reconnect attempts reached')
this.emit('maxReconnect')
return
}
this.reconnectAttempts++
const interval = Math.min(
this.options.reconnectInterval *
Math.pow(this.options.reconnectDecay, this.reconnectAttempts - 1),
this.options.maxReconnectInterval
)
console.log(`Reconnecting in ${interval}ms... (attempt ${this.reconnectAttempts})`)
this.reconnectTimer = setTimeout(() => {
this.emit('reconnecting', this.reconnectAttempts)
this.connect()
}, interval)
}
startHeartbeat() {
this.stopHeartbeat()
this.heartbeatTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.send(this.options.heartbeatMessage)
}
}, this.options.heartbeatInterval)
}
resetHeartbeat() {
this.stopHeartbeat()
this.startHeartbeat()
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
const message = typeof data === 'string' ? data : JSON.stringify(data)
this.ws.send(message)
return true
}
console.warn('WebSocket is not open')
return false
}
close(code = 1000, reason = 'Normal closure') {
this.forcedClose = true
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
}
this.stopHeartbeat()
if (this.ws) {
this.ws.close(code, reason)
}
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, [])
}
this.listeners.get(event).push(callback)
}
off(event, callback) {
if (!this.listeners.has(event)) return
const callbacks = this.listeners.get(event)
const index = callbacks.indexOf(callback)
if (index !== -1) {
callbacks.splice(index, 1)
}
}
emit(event, data) {
if (!this.listeners.has(event)) return
this.listeners.get(event).forEach(callback => {
try {
callback(data)
} catch (error) {
console.error('Error in event listener:', error)
}
})
}
}
// 使用示例
const ws = new ReliableWebSocket('wss://api.example.com/ws', {
reconnectInterval: 1000,
maxReconnectAttempts: 10,
heartbeatInterval: 30000
})
ws.on('open', () => {
console.log('Connected to server')
ws.send({ type: 'subscribe', channel: 'updates' })
})
ws.on('message', (data) => {
console.log('Received:', data)
})
ws.on('error', (error) => {
console.error('WebSocket error:', error)
})
ws.on('close', (event) => {
console.log('Connection closed:', event)
})
ws.on('reconnecting', (attempt) => {
console.log('Reconnecting... attempt', attempt)
})
// React Hook 封装
import { useEffect, useRef, useState } from 'react'
function useWebSocket(url, options = {}) {
const [data, setData] = useState(null)
const [status, setStatus] = useState('connecting')
const wsRef = useRef(null)
useEffect(() => {
const ws = new ReliableWebSocket(url, options)
wsRef.current = ws
ws.on('open', () => setStatus('connected'))
ws.on('message', (data) => setData(data))
ws.on('close', () => setStatus('disconnected'))
ws.on('reconnecting', () => setStatus('reconnecting'))
return () => {
ws.close()
}
}, [url])
const send = (data) => {
wsRef.current?.send(data)
}
return { data, status, send }
}
// 使用
function Chat() {
const { data, status, send } = useWebSocket('wss://api.example.com/chat')
const handleSend = () => {
send({ type: 'message', content: 'Hello' })
}
return (
<div>
<div>Status: {status}</div>
<div>Latest message: {JSON.stringify(data)}</div>
<button onClick={handleSend}>Send</button>
</div>
)
}