阶段 2:框架原理 - 从"会用"到"懂原理"
你提到的这几个点确实是区分"熟练工"和"懂原理的人"的分水岭。我来逐一拆解。
一、React 为什么快?—— 先破题
这个问题本身就是个陷阱题。正确的回答思路:
React 并不总是快,它的优势在于通过巧妙的设计,在保证开发体验的前提下,把"哪里需要更新"的决定权交给算法,避免不了的重渲染就用时间来换空间(Fiber)。
核心答案框架:
- 虚拟 DOM + Diff → 减少 DOM 操作
- Fiber 架构 → 可中断的更新,不阻塞用户交互
- 调度机制 → 优先级控制,高优先级任务插队
二、React Fiber —— 最核心的突破
2.1 为什么需要 Fiber?
旧版(Stack Reconciler)的问题:
- 递归协调,一旦开始更新就无法中断
- 大组件树更新时,浏览器掉帧(超过 16ms)
- 用户点击、输入等交互被阻塞
Fiber 的解决方案:
将协调过程拆分成一个个小任务(Fiber节点),任务之间可以中断、恢复、优先级调整。
2.2 Fiber 是什么?
数据结构层面: 每个 React 元素对应一个 Fiber 节点,是一个 JS 对象
// 简化的 Fiber 节点结构
{
// 类型信息
tag: HostComponent, // 节点类型(div、span等)
type: 'div',
key: null,
// 树结构关系
return: parentFiber, // 父节点
child: firstChildFiber, // 第一个子节点
sibling: nextSibling, // 下一个兄弟节点
// 状态
memoizedState: null, // 当前渲染的状态(hooks 链表头)
memoizedProps: null, // 当前渲染的 props
pendingProps: null, // 等待处理的 props
// 副作用
flags: Update, // 要执行的操作(增删改)
subtreeFlags: null, // 子树需要执行的操作
// 双缓存
alternate: oldFiber, // 指向上一棵树的对应节点
}
2.3 Fiber 的工作流程(双缓存机制)
React 在内存中维护两棵 Fiber 树:
- Current 树:当前屏幕上显示的内容
- WorkInProgress 树:正在构建的新树(在内存中)
// 简化版 Fiber 工作循环
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
// 执行当前单元,并返回下一个要执行的单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
// 检查剩余时间是否足够
shouldYield = deadline.timeRemaining() < 1
}
// 如果所有工作完成,提交到 DOM
if (!nextUnitOfWork && workInProgressRoot) {
commitRoot()
}
// 让出控制权,等待下一帧
requestIdleCallback(workLoop)
}
// 开始工作
requestIdleCallback(workLoop)
关键点:
performUnitOfWork执行一个 Fiber 节点(处理完后返回下一个)- 通过
requestIdleCallback(React 实际用的是MessageChannel+ 自己实现的时间切片)在浏览器空闲时执行 - 每执行完一个节点就检查是否超时,超时则中断
三、React Diff 算法 —— O(n³) → O(n)
3.1 三个大胆的假设
- 只比较同层级节点,不跨层比较
- 不同类型元素产生不同树(div → p 直接销毁重建)
- 通过 key 标识子节点稳定性
3.2 Diff 流程
// 简化的 reconcileChildFibers 逻辑
function reconcileChildren(returnFiber, currentFirstChild, newChildren) {
// 情况1:新节点是单节点(原生元素/组件)
if (isSingleElement(newChildren)) {
// 尝试复用(key 和 type 都相同)
if (currentFirstChild && currentFirstChild.key === newChildren.key) {
if (currentFirstChild.type === newChildren.type) {
// 复用,更新 props
return existingFiber
} else {
// 类型不同,删除旧节点,创建新节点
deleteRemainingChildren(currentFirstChild)
return createNewFiber(newChildren)
}
}
}
// 情况2:新节点是多节点(数组)
// 使用三指针遍历
let newIdx = 0
let oldFiber = currentFirstChild
let lastPlacedIndex = 0 // 记录上次可复用节点的位置
// 第一轮:同时遍历新旧节点,遇到 key 不同的就跳出
for (; oldFiber && newIdx < newChildren.length; newIdx++) {
if (oldFiber.key !== newChildren[newIdx].key) {
break
}
if (oldFiber.type === newChildren[newIdx].type) {
// 可复用,更新位置标记
lastPlacedIndex = Math.max(lastPlacedIndex, oldFiber.index)
}
oldFiber = oldFiber.sibling
}
// 第二轮:处理剩余节点(标记删除或移动)
if (newIdx === newChildren.length) {
// 新节点遍历完,删除剩余旧节点
deleteRemainingChildren(oldFiber)
}
}
3.3 为什么不建议用 index 作为 key?
// 列表:[A, B, C]
// 头部插入 D → [D, A, B, C]
// 用 index 作为 key
// A(0) → D(0) key 相同但内容不同,无法复用
// B(1) → A(1) key 相同但内容不同,无法复用
// 结果:全部重建
// 用 id 作为 key
// A(id_a) → A(id_a) 复用
// B(id_b) → B(id_b) 复用
// C(id_c) → C(id_c) 复用
// 只新增 D,其他全部复用
四、Hooks 原理 —— 为什么不能在条件语句中使用?
4.1 Hooks 的本质
Hooks 是一个存储在 Fiber 节点上的链表:
// Fiber 节点中的 memoizedState 指向 hooks 链表
{
memoizedState: { // 第一个 hook
memoizedState: 'value', // hook 存储的值(useState 的 state)
next: { // 第二个 hook
memoizedState: 'value2',
next: { ... } // 第三个 hook
}
}
}
4.2 useState 简化实现
// React 内部简化版
let currentlyRenderingFiber = null
let workInProgressHook = null
function renderWithHooks(fiber) {
currentlyRenderingFiber = fiber
currentlyRenderingFiber.memoizedState = null
workInProgressHook = null
// ... 执行组件函数
}
function useState(initialState) {
// 获取当前 hook
const hook = getOrCreateHook()
// 如果是首次渲染,初始化 state
if (hook.memoizedState === undefined) {
hook.memoizedState = typeof initialState === 'function'
? initialState()
: initialState
}
// 定义 setter
const setState = (action) => {
const newState = typeof action === 'function'
? action(hook.memoizedState)
: action
hook.memoizedState = newState
// 触发更新(将当前 fiber 标记为需要更新)
scheduleUpdateOnFiber(currentlyRenderingFiber)
}
return [hook.memoizedState, setState]
}
function getOrCreateHook() {
if (workInProgressHook === null) {
// 第一个 hook
workInProgressHook = currentlyRenderingFiber.memoizedState
return workInProgressHook
} else {
// 后续 hook
workInProgressHook = workInProgressHook.next
return workInProgressHook
}
}
4.3 为什么不能在条件语句中调用 Hook?
// ❌ 错误
function Component({ flag }) {
const [a, setA] = useState(0)
if (flag) {
const [b, setB] = useState(1) // 条件性调用
}
const [c, setC] = useState(2)
}
// 第一次渲染 flag=true:hooks 链表 [a, b, c]
// 第二次渲染 flag=false:hooks 链表 [a, c],但 React 以为第三个 hook 是 c
// 结果:状态错乱(c 拿到了 b 的状态)
React 依赖 Hook 的调用顺序来匹配状态,破坏顺序就会出 Bug。
五、调度机制(Scheduler)
5.1 优先级体系
// React 内部的优先级定义
const ImmediatePriority = 1 // 立即执行(用户点击、输入)
const UserBlockingPriority = 2 // 用户交互(hover、滚动)
const NormalPriority = 3 // 普通更新(setState)
const LowPriority = 4 // 低优先级(数据预取)
const IdlePriority = 5 // 空闲执行(日志上报)
5.2 时间切片 + 优先级插队
// 简化版调度逻辑
function ensureRootIsScheduled(root) {
const existingCallback = root.callbackNode
// 获取当前最高优先级的更新
const newCallbackPriority = getHighestPriorityUpdate(root)
// 如果没有更高优先级的任务,返回
if (newCallbackPriority === existingCallbackPriority) return
// 取消正在进行的任务(可中断)
if (existingCallback) {
cancelCallback(existingCallback)
}
// 根据优先级决定调度方式
let newCallback
if (newCallbackPriority === ImmediatePriority) {
// 同步执行,不等待
newCallback = scheduleSyncCallback(performWorkOnRoot.bind(null, root))
} else {
// 异步执行,使用 MessageChannel 实现宏任务
newCallback = scheduleCallback(
newCallbackPriority,
performConcurrentWorkOnRoot.bind(null, root)
)
}
root.callbackNode = newCallback
}
六、Vue 响应式原理(对照学习)
6.1 Vue 2 vs Vue 3
| 特性 | Vue 2 (Object.defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 监听数组 | 需要重写 7 个数组方法 | 直接支持 |
| 新增属性 | 需要 $set | 自动响应 |
| 删除属性 | 无法监听 | 可监听 |
| 性能 | 递归遍历所有属性 | 懒代理,访问时才递归 |
6.2 Proxy 简化实现
function reactive(target) {
if (typeof target !== 'object' || target === null) {
return target
}
const handler = {
get(obj, key, receiver) {
console.log(`读取 ${key}`)
// 依赖收集(track)
track(obj, key)
const result = Reflect.get(obj, key, receiver)
// 深度代理(懒代理,性能更好)
return typeof result === 'object' ? reactive(result) : result
},
set(obj, key, value, receiver) {
const oldValue = obj[key]
const result = Reflect.set(obj, key, value, receiver)
if (oldValue !== value) {
console.log(`修改 ${key} = ${value}`)
// 触发更新(trigger)
trigger(obj, key)
}
return result
},
deleteProperty(obj, key) {
const hadKey = Object.prototype.hasOwnProperty.call(obj, key)
const result = Reflect.deleteProperty(obj, key)
if (hadKey && result) {
trigger(obj, key)
}
return result
}
}
return new Proxy(target, handler)
}
七、面试总结
当被问到 "React 为什么快" 时,回答话术:
"严格来说 React 并不是'快',而是'感知上流畅'。它做了三件事:
- Fiber 架构:把更新拆成可中断的小任务,用
requestIdleCallback在浏览器空闲时执行,避免掉帧。- Diff 算法:基于三个假设将 O(n³) 降为 O(n),通过 key 最大化复用 DOM。
- 优先级调度:用户交互(点击、输入)优先级高于普通 setState,高优先级可以打断低优先级更新。
但 React 也有代价——它无法做到细粒度的更新,Vue 的响应式系统在这方面更精确。React 是用'按需计算 + 时间切片'换来了复杂应用的可维护性。"
常见追问及回答:
| 追问 | 回答要点 |
|---|---|
| "Fiber 和普通递归有什么区别?" | 递归不可中断,Fiber 用链表 + 循环 + 时间检查实现可中断 |
| "为什么不用 Web Worker?" | DOM 操作必须在主线程,Web Worker 无法操作 DOM |
| "React 18 的并发有什么新东西?" | startTransition、useTransition、useDeferredValue 让开发者主动标记非紧急更新 |