框架原理深度解析
前端面试题 - 框架原理深度解析(React vs Vue)
本章节深度解析 React 和 Vue 的核心原理,包括虚拟 DOM、Diff 算法、响应式系统、Fiber 架构、Hooks 原理等。适合高级前端工程师面试准备。
📊 数据统计
| 统计项 | 数值 |
|---|---|
| 总题数 | 60道 |
| 基础题 | 15道 (25%) |
| 中级题 | 28道 (46.7%) |
| 高级题 | 17道 (28.3%) |
📚 目录
一、虚拟 DOM 原理
1. 什么是虚拟 DOM?为什么要使用它?
难度:⭐⭐(中级)
标签:#虚拟DOM
参考答案要点:
定义:虚拟 DOM 是用 JavaScript 对象描述真实 DOM 结构的轻量级表示。
// 真实 DOM
;<div id="app" class="container">
<h1>Hello</h1>
<p>World</p>
</div>
// 虚拟 DOM
const vdom = {
tag: "div",
props: { id: "app", class: "container" },
children: [
{ tag: "h1", props: {}, children: ["Hello"] },
{ tag: "p", props: {}, children: ["World"] },
],
}使用原因:
| 优势 | 说明 |
|---|---|
| 性能优化 | 减少直接操作 DOM,批量更新 |
| 跨平台 | 虚拟 DOM 可映射到不同平台(Web/Native/小程序) |
| 开发体验 | 声明式编程,无需手动操作 DOM |
| 一致性 | 保证 UI 与状态同步 |
为什么不直接操作 DOM?
- DOM 操作是昂贵的(重排重绘)
- 虚拟 DOM 可以批量处理更新
- 通过 Diff 算法最小化 DOM 操作
2. 虚拟 DOM 真的比直接操作 DOM 快吗?
难度:⭐⭐⭐(高级)
标签:#虚拟DOM #性能
参考答案要点:
不是绝对的,虚拟 DOM 的优势在于批量更新和跨平台,而非单纯的速度。
场景对比:
| 场景 | 直接操作 DOM | 虚拟 DOM |
|---|---|---|
| 单次更新 | 快 | 慢(需要生成 VDOM + Diff) |
| 批量更新 | 慢(多次重排) | 快(合并更新) |
| 复杂应用 | 难以优化 | 自动优化 |
虚拟 DOM 的价值:
- 开发效率:声明式编程,专注状态管理
- 自动优化:Diff 算法找出最小更新
- 跨平台:同一套代码运行在不同环境
结论:虚拟 DOM 是为了开发体验和跨平台,性能是副产品。
3. Vue 和 React 的虚拟 DOM 有什么区别?
难度:⭐⭐⭐(高级)
标签:#虚拟DOM #React #Vue
参考答案要点:
| 特性 | React | Vue |
|---|---|---|
| 创建方式 | JSX + createElement | Template 编译 |
| 编译优化 | 较少(依赖开发者) | 较多(编译时优化) |
| 更新粒度 | 组件级别 | 组件级别 |
| 静态提升 | 无 | 有(Vue 3) |
| PatchFlag | 无 | 有(Vue 3) |
Vue 3 编译优化:
<template>
<div>
<h1>静态标题</h1>
<!-- 静态提升,跳过 Diff -->
<p>{{ dynamic }}</p>
<!-- 标记为动态,只比较这里 -->
</div>
</template>编译后会标记动态节点,Diff 时只比较动态部分。
二、Diff 算法对比
4. React 的 Diff 算法原理是什么?
难度:⭐⭐⭐(高级)
标签:#Diff #React
参考答案要点:
核心策略:
-
树 Diff:
- 只比较同一层级的节点
- 跨层级移动节点,直接销毁重建
-
组件 Diff:
- 同一类型组件:继续 Diff 子树
- 不同类型组件:销毁旧组件,创建新组件
-
元素 Diff:
- 通过 key 识别节点
- 三种操作:插入、移动、删除
Diff 流程:
// 简化版 Diff
function diff(oldNode, newNode) {
// 1. 类型不同,直接替换
if (oldNode.type !== newNode.type) {
return { type: "REPLACE", newNode }
}
// 2. 文本节点
if (typeof oldNode === "string") {
if (oldNode !== newNode) {
return { type: "TEXT", content: newNode }
}
return null
}
// 3. 比较属性
const propsPatches = diffProps(oldNode.props, newNode.props)
// 4. 比较子节点
const childrenPatches = diffChildren(oldNode.children, newNode.children)
return { type: "UPDATE", props: propsPatches, children: childrenPatches }
}5. Vue 的 Diff 算法原理是什么?
难度:⭐⭐⭐(高级)
标签:#Diff #Vue
参考答案要点:
核心策略:
- 同层比较:只比较同一层级的节点
- 双端比较:新旧列表从两端向中间比较
- key 优化:通过 key 复用节点
双端比较算法:
旧列表:[a, b, c, d]
新列表:[b, a, d, c]
步骤:
1. 旧头 a vs 新头 b → 不同
2. 旧头 a vs 新尾 c → 不同
3. 旧尾 d vs 新头 b → 不同
4. 旧尾 d vs 新尾 c → 不同
5. 从旧列表找 b → 找到,移动到头部
6. 继续比较...Vue 2 vs Vue 3:
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 算法 | 双端比较 | 双端比较 + 最长递增子序列 |
| 静态节点 | 无优化 | 静态提升 |
| PatchFlag | 无 | 有,标记动态内容 |
6. React 和 Vue 的 Diff 算法有什么区别?
难度:⭐⭐⭐(高级)
标签:#Diff #React #Vue
参考答案要点:
| 特性 | React | Vue |
|---|---|---|
| 比较方向 | 从左到右 | 双端比较 |
| 列表优化 | 需要 key | 需要 key |
| 移动检测 | 较弱 | 较强(双端) |
| 时间复杂度 | O(n) | O(n) |
React 的不足:
- 只能从左到右比较
- 列表末尾移动到开头,会全部重建
Vue 的优势:
- 双端比较,能更快找到可复用节点
- 使用最长递增子序列优化移动操作
示例对比:
// 旧列表:[a, b, c]
// 新列表:[c, a, b]
// React:全部重建(无法识别移动)
// Vue:识别移动,复用节点7. 为什么列表需要 key?index 作为 key 有什么问题?
难度:⭐⭐(中级)
标签:#Diff #key
参考答案要点:
key 的作用:
- 唯一标识:帮助框架识别每个节点的身份
- 复用节点:通过 key 找到对应节点,避免销毁重建
- 保持状态:确保组件状态与正确的 DOM 关联
index 作为 key 的问题:
// 列表:[{id: 1, name: 'A'}, {id: 2, name: 'B'}]
// 渲染:<li key={index}>{item.name}</li>
// 在头部插入新项
// 新列表:[{id: 3, name: 'C'}, {id: 1, name: 'A'}, {id: 2, name: 'B'}]
// 问题:
// key 0: A → C(复用错误,状态混乱)
// key 1: B → A(复用错误)
// key 2: 无 → B(新建)
// 结果:全部更新,性能差,状态混乱正确做法:使用唯一 ID 作为 key
<li key={item.id}>{item.name}</li>三、响应式系统
8. Vue 2 的响应式原理是什么?
难度:⭐⭐⭐(高级)
标签:#响应式 #Vue2
参考答案要点:
核心机制:数据劫持 + 发布订阅模式
// 简化版实现
function defineReactive(obj, key, val) {
const dep = new Dep() // 依赖收集器
Object.defineProperty(obj, key, {
get() {
// 收集依赖
if (Dep.target) {
dep.depend()
}
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
// 通知更新
dep.notify()
}
},
})
}
class Dep {
constructor() {
this.subs = []
}
depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
notify() {
this.subs.forEach((watcher) => watcher.update())
}
}局限性:
- 无法检测对象属性的新增/删除
- 无法检测数组索引的变化
- 需要遍历对象所有属性,性能开销
9. Vue 3 的响应式原理是什么?
难度:⭐⭐⭐(高级)
标签:#响应式 #Vue3
参考答案要点:
核心机制:Proxy + Reflect
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key) // 依赖收集
return Reflect.get(target, key, receiver)
},
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
},
})
}优势对比:
| 特性 | Vue 2 (defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 新增属性 | 无法监听 | 自动监听 |
| 删除属性 | 无法监听 | 自动监听 |
| 数组索引 | 无法监听 | 自动监听 |
| Map/Set | 不支持 | 支持 |
| 性能 | 初始化遍历所有属性 | 懒代理,按需触发 |
10. React 的 setState 是同步还是异步的?
难度:⭐⭐⭐(高级)
标签:#setState #React
参考答案要点:
既可能是同步,也可能是异步,取决于执行上下文。
异步情况(默认):
// React 事件处理函数中
handleClick = () => {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 旧值(异步)
}同步情况:
// setTimeout 中
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 新值(同步)
}, 0)
// 原生事件监听中
document.addEventListener("click", () => {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 新值(同步)
})原因:
- React 事件处理函数中:批量更新(Batch Update)机制
- setTimeout/原生事件:脱离 React 控制,同步执行
React 18 的改进:
- 自动批处理(Automatic Batching)
- 所有情况下都是异步的
11. React 的批量更新(Batch Update)机制是什么?
难度:⭐⭐⭐(高级)
标签:#批量更新 #React
参考答案要点:
定义:将多个 setState 合并为一次更新,减少渲染次数。
// 批量更新前(3 次渲染)
handleClick = () => {
this.setState({ a: 1 })
this.setState({ b: 2 })
this.setState({ c: 3 })
}
// 批量更新后(1 次渲染)
handleClick = () => {
this.setState({ a: 1 })
this.setState({ b: 2 })
this.setState({ c: 3 })
// 合并为 { a: 1, b: 2, c: 3 },只渲染一次
}实现原理:
// 简化版实现
let isBatchingUpdates = false
let pendingState = []
function setState(newState) {
if (isBatchingUpdates) {
// 批量更新中,暂存状态
pendingState.push(newState)
} else {
// 直接更新
updateComponent(newState)
}
}
function batchedUpdates(fn) {
isBatchingUpdates = true
fn()
isBatchingUpdates = false
// 合并所有状态,执行一次更新
const finalState = Object.assign({}, ...pendingState)
pendingState = []
updateComponent(finalState)
}React 18 自动批处理:
// React 18 中,所有情况都自动批处理
setTimeout(() => {
setCount((c) => c + 1)
setFlag((f) => !f)
// 只渲染一次
}, 1000)四、React Fiber 架构
12. 什么是 React Fiber?为什么要引入?
难度:⭐⭐⭐(高级)
标签:#Fiber #React
参考答案要点:
Fiber 定义:
- 新的协调引擎(Reconciliation)
- 重新设计的组件树遍历算法
- 支持可中断和恢复的渲染
为什么需要 Fiber:
React 15 的问题:
- 递归遍历组件树,无法中断
- 大量组件更新时,阻塞主线程
- 页面卡顿,用户体验差
Fiber 的解决方案:
- 将渲染工作拆分为小单元
- 每个单元执行后检查是否有高优先级任务
- 有则暂停渲染,让出主线程
React 15: 递归渲染(阻塞)
├─ 渲染 A
│ ├─ 渲染 B
│ │ ├─ 渲染 C(耗时 200ms,页面卡顿)
│ │ └─ 渲染 D
│ └─ 渲染 E
└─ 渲染 F
React 16+: Fiber(可中断)
├─ 渲染 A → 检查 → 继续
├─ 渲染 B → 检查 → 继续
├─ 渲染 C → 检查 → 有高优先级任务 → 暂停
├─ 执行高优先级任务
└─ 恢复渲染 C → 完成13. Fiber 的工作原理是什么?
难度:⭐⭐⭐⭐(专家)
标签:#Fiber #React
参考答案要点:
Fiber 结构:
interface Fiber {
type: any, // 组件类型
key: null | string, // key
stateNode: any, // 真实 DOM 或组件实例
// 链表指针
return: Fiber | null, // 父节点
child: Fiber | null, // 第一个子节点
sibling: Fiber | null, // 下一个兄弟节点
// 工作单元
pendingProps: any, // 新 props
memoizedProps: any, // 旧 props
memoizedState: any, // 旧 state
// 副作用
effectTag: number, // 副作用类型(增删改)
nextEffect: Fiber | null, // 下一个副作用节点
}双缓冲技术:
current Fiber Tree(当前显示)
↓
workInProgress Fiber Tree(正在构建)
↓
渲染完成,交换指针
↓
workInProgress 变为 current工作流程:
-
Render 阶段(可中断):
- 构建 Fiber 树
- 计算变化(Diff)
- 标记副作用
-
Commit 阶段(不可中断):
- 执行副作用(DOM 操作)
- 执行生命周期
- 执行回调
14. Fiber 如何实现时间切片?
难度:⭐⭐⭐⭐(专家)
标签:#Fiber #时间切片
参考答案要点:
时间切片:将长任务拆分为多个小任务,每执行完一个检查是否超时。
实现原理:
// 简化版实现
const FRAME_DURATION = 5 // 每帧最多 5ms 用于 React 渲染
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
// 执行一个 Fiber 单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
// 检查是否超时
shouldYield = deadline.timeRemaining() < FRAME_DURATION
}
if (nextUnitOfWork) {
// 还有工作,请求下一帧继续
requestIdleCallback(workLoop)
} else {
// 完成,提交更改
commitRoot()
}
}
// 启动
requestIdleCallback(workLoop)优先级调度:
// 不同优先级的更新
const priorities = {
ImmediatePriority: 1, // 立即执行(用户输入)
UserBlockingPriority: 2, // 用户阻塞(点击)
NormalPriority: 3, // 正常优先级
LowPriority: 4, // 低优先级
IdlePriority: 5, // 空闲时执行
}五、Hooks 原理
15. React Hooks 的原理是什么?
难度:⭐⭐⭐⭐(专家)
标签:#Hooks #React
参考答案要点:
Hooks 本质:利用闭包和数组来保存状态。
实现原理(简化版):
// React 内部实现
const React = (function () {
let hooks = [] // 存储所有 state
let idx = 0 // 当前 hook 的索引
function useState(initialValue) {
const state = hooks[idx] !== undefined ? hooks[idx] : initialValue
hooks[idx] = state
const _idx = idx
idx++
function setState(newValue) {
hooks[_idx] = newValue
render() // 重新渲染
}
return [state, setState]
}
function render(Component) {
idx = 0 // 每次渲染重置索引
const comp = Component()
comp.render()
return comp
}
return { useState, render }
})()
// 使用
function MyComponent() {
const [count, setCount] = React.useState(0)
const [name, setName] = React.useState("React")
return {
render: () => {
console.log({ count, name })
},
click: () => setCount(count + 1),
}
}为什么不能在 if 中使用 Hooks:
- Hooks 依赖调用顺序
- if 中调用会导致索引错乱
- 状态与组件对应关系混乱
16. useEffect 和 useLayoutEffect 的区别?
难度:⭐⭐⭐(高级)
标签:#Hooks #Effect
参考答案要点:
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 渲染完成后异步执行 | 渲染完成后同步执行(浏览器绘制前) |
| 阻塞渲染 | 否 | 是(可能导致卡顿) |
| 使用场景 | 大多数副作用 | 需要同步执行的副作用(测量 DOM) |
执行流程对比:
useEffect:
组件渲染 → 绘制到屏幕 → 执行 useEffect
useLayoutEffect:
组件渲染 → 执行 useLayoutEffect → 绘制到屏幕使用建议:
- 优先使用 useEffect
- 只有出现视觉闪烁时,才使用 useLayoutEffect
17. useMemo 和 useCallback 的区别?
难度:⭐⭐(中级)
标签:#Hooks #性能优化
参考答案要点:
| 特性 | useMemo | useCallback |
|---|---|---|
| 缓存内容 | 计算结果 | 函数本身 |
| 返回值 | 计算结果 | 函数 |
| 使用场景 | 昂贵的计算 | 子组件回调、依赖数组 |
关系:
useCallback(fn, deps) === useMemo(() => fn, deps)使用示例:
// useMemo:缓存计算结果
const expensiveValue = useMemo(() => {
return computeExpensiveValue(a, b)
}, [a, b])
// useCallback:缓存函数
const handleClick = useCallback(() => {
doSomething(a, b)
}, [a, b])六、组件更新机制
18. React 组件何时会重新渲染?
难度:⭐⭐(中级)
标签:#渲染 #React
参考答案要点:
触发重新渲染的情况:
-
state 变化:
JAVASCRIPTsetState({ count: 1 }) // 重新渲染 -
props 变化:
JAVASCRIPT// 父组件传递新的 props <Child name="new" /> -
父组件重新渲染:
JAVASCRIPT// 即使 props 不变,父组件渲染子组件也会渲染 -
forceUpdate:
JAVASCRIPTthis.forceUpdate() // 强制重新渲染
优化手段:
- React.memo(函数组件)
- shouldComponentUpdate/PureComponent(类组件)
- useMemo/useCallback
19. Vue 组件何时会重新渲染?
难度:⭐⭐(中级)
标签:#渲染 #Vue
参考答案要点:
触发重新渲染的情况:
-
响应式数据变化:
JAVASCRIPTthis.count++ // 触发重新渲染 -
props 变化:
JAVASCRIPT// 父组件传递新的 props -
父组件重新渲染:
JAVASCRIPT// 子组件会重新渲染
优化手段:
- computed(缓存计算结果)
- watch(精确控制更新)
- v-once(只渲染一次)
- shouldComponentUpdate(React 概念,Vue 无直接对应)
20. React.memo 和 Vue 的 computed 有什么区别?
难度:⭐⭐⭐(高级)
标签:#性能优化 #React #Vue
参考答案要点:
| 特性 | React.memo | Vue computed |
|---|---|---|
| 作用 | 缓存组件渲染结果 | 缓存计算结果 |
| 比较方式 | 浅比较 props | 依赖追踪 |
| 使用场景 | 组件级优化 | 数据计算优化 |
| 自定义比较 | 支持自定义函数 | 不支持 |
React.memo 示例:
const MyComponent = React.memo(
function MyComponent({ name }) {
return <div>{name}</div>
},
(prevProps, nextProps) => {
// 自定义比较
return prevProps.name === nextProps.name
}
)Vue computed 示例:
const fullName = computed(() => {
return firstName.value + " " + lastName.value
})七、性能优化原理
21. React 的 Time Slicing(时间切片)是什么?
难度:⭐⭐⭐(高级)
标签:#性能优化 #React
参考答案要点:
定义:将渲染工作拆分为多个小任务,分散到多个帧中执行。
与 Fiber 的关系:
- Fiber 是架构基础
- Time Slicing 是 Fiber 的能力之一
实现原理:
// 使用 requestIdleCallback 或 MessageChannel
function workLoop(hasTimeRemaining) {
while (nextUnitOfWork && hasTimeRemaining()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
if (nextUnitOfWork) {
// 时间用完,下一帧继续
scheduleCallback(workLoop)
}
}用户感知:
- 大量数据渲染时,页面不会卡顿
- 用户输入响应更及时
22. Vue 3 的静态提升(Static Hoisting)是什么?
难度:⭐⭐⭐(高级)
标签:#性能优化 #Vue3
参考答案要点:
定义:编译阶段将静态节点提取出来,避免每次渲染重新创建。
示例:
<template>
<div>
<h1>静态标题</h1>
<!-- 静态节点 -->
<p>{{ dynamic }}</p>
<!-- 动态节点 -->
</div>
</template>编译结果:
const _hoisted_1 = /*#__PURE__*/ createElementVNode(
"h1",
null,
"静态标题",
-1 /* HOISTED */
)
export function render(_ctx, _cache) {
return (
openBlock(),
createElementBlock("div", null, [
_hoisted_1, // 直接复用静态节点
createElementVNode(
"p",
null,
toDisplayString(_ctx.dynamic),
1 /* TEXT */
),
])
)
}优势:
- 减少内存分配
- 减少 Diff 工作量
- 提升渲染性能
23. React 的 Concurrent Mode 是什么?
难度:⭐⭐⭐⭐(专家)
标签:#ConcurrentMode #React
参考答案要点:
定义:React 18 引入的新特性,使渲染可中断,提升用户体验。
核心特性:
-
可中断渲染:
- 渲染过程中可以暂停
- 优先处理高优先级任务(用户输入)
-
自动批处理:
- 所有更新自动批处理
- 减少渲染次数
-
Suspense:
- 更好的异步处理
- 数据获取与渲染分离
使用方式:
// React 18 默认启用
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App />)
// useTransition:标记非紧急更新
const [isPending, startTransition] = useTransition()
startTransition(() => {
setSearchQuery(input) // 非紧急更新,可中断
})复习建议
重点题目清单
必会题目(⭐⭐⭐ 高频)
- 虚拟 DOM 原理和优势
- React 和 Vue 的 Diff 算法区别
- key 的作用和注意事项
- Vue 2 和 Vue 3 响应式原理
- setState 同步/异步问题
- Fiber 架构和优势
- Hooks 原理和使用规则
重点掌握(⭐⭐ 常考)
- 批量更新机制
- useEffect vs useLayoutEffect
- useMemo vs useCallback
- 组件更新机制
- 性能优化手段
加分项(⭐ 了解)
- 时间切片实现
- Concurrent Mode
- 静态提升原理
学习建议
-
阅读源码:
- Vue 3 响应式系统(reactivity 包)
- React Fiber 实现(ReactFiber 文件)
-
画图理解:
- Diff 过程
- Fiber 链表结构
- 响应式依赖收集
-
手写实现:
- 简化版 Vue 响应式
- 简化版 React Hooks
- 虚拟 DOM + Diff