React 的核心不仅在于它的组件化思想,更在于其底层维持高性能和响应性的精妙算法设计。在面试和实际架构理解中,核心算法主要围绕 找出差异(Diffing)、任务调度(Fiber)、优先级管理(Lanes) 以及 状态闭包与存取(Hooks 机制) 展开。
为了从根本上理解 React 的运行机制,我们可以将这些算法解构为以下四个核心部分:
1. 协调算法 (Reconciliation / Diffing)
传统的树对比算法复杂度是 ,这对于前端 DOM 树来说性能代价太高。React 的 Diff 算法基于三个“妥协性”的启发式前提,硬生生将复杂度降到了 :
-
Tree Diff (层级对比): DOM 节点跨层级的移动操作少到可以忽略不计。因此,React 只会对同级节点进行比较。
-
Component Diff (组件对比): 拥有相同类的两个组件会生成相似的树形结构,拥有不同类的两个组件会生成不同的树形结构。
-
Element Diff (元素对比): 对于同一层级的一组子节点,它们可以通过唯一
key进行区分。
Mermaid 图表:层级对比逻辑
代码模拟:同级双指针 Diff (常用于列表更新)
JavaScript
// React 实际使用的是单链表遍历,这里为了直观展示 Diff 核心思想(类似 Vue 的双端或 React 的按索引查找)
function reconcileChildren(oldChildren, newChildren) {
let lastPlacedIndex = 0; // 记录上一个不需要移动的旧节点索引
for (let i = 0; i < newChildren.length; i++) {
const newChild = newChildren[i];
const oldChild = findOldChildByKey(oldChildren, newChild.key); // 通过 key 查找
if (oldChild) {
// 找到了,说明可以复用
if (oldChild.index < lastPlacedIndex) {
// 旧节点原本在左边,现在由于前面的节点变化,需要向右移动
markAsMoved(oldChild);
} else {
// 不需要移动,更新位置基准
lastPlacedIndex = oldChild.index;
}
updateComponent(oldChild, newChild); // 仅更新属性
} else {
// 没找到,新增节点
markAsInserted(newChild);
}
}
}
2. 可中断渲染架构 (Fiber 算法)
在 React 15 及以前,由于采用递归遍历虚拟 DOM,一旦开始就无法中断。如果树很深,主线程会被长时间阻塞,导致掉帧(卡顿)。React 16 引入了 Fiber,本质是将递归变成了循环,将树的结构变成了单链表结构。
Fiber 的核心思想是时间切片(Time Slicing):将渲染工作拆分成小任务,每执行完一个小任务就看看主线程是否有更高优先级的任务(如用户输入、动画),如果有,就让出控制权。
Mermaid 图表:Fiber 的链表树结构
代码模拟:可中断的工作循环
JavaScript
let nextUnitOfWork = null; // 指向下一个 Fiber 节点
// 浏览器的 requestIdleCallback 会在浏览器空闲时调用此函数
function workLoop(deadline) {
let shouldYield = false; // 是否需要让出控制权
// 只要有任务且不需要让出控制权,就一直执行
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 执行当前任务,并返回下一个任务
// 检查剩余时间,如果不足则让出
shouldYield = deadline.timeRemaining() < 1;
}
if (nextUnitOfWork) {
// 任务没做完,请求下一次浏览器空闲时继续
requestIdleCallback(workLoop);
}
}
function performUnitOfWork(fiber) {
// 1. 创建 DOM,处理当前 fiber 节点的更新
// 2. 将子节点转换为新的 fiber 节点(Diffing 发生在这里)
// 3. 返回下一个要处理的 fiber (深度优先:child -> sibling -> return)
if (fiber.child) return fiber.child;
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) return nextFiber.sibling;
nextFiber = nextFiber.return;
}
return null;
}
3. 位运算调度优先级 (Lanes 模型)
在并发模式下,不仅需要打断,还需要决定“先执行谁”。早期的 ExpirationTime(过期时间)模型在处理批量更新和复杂优先级时不够灵活。React 17+ 引入了 Lanes 模型,借鉴了车道的概念,利用32位整数的位运算来管理优先级。
位运算的优势在于速度极快,且能用按位与(&)、按位或(|)轻松处理批量的任务合并与分离。
Mermaid 图表:优先级插队处理
代码模拟:Lanes 位运算
JavaScript
// 不同的数字占用不同的位(二进制表示)
const SyncLane = 0b00001; // 最高优先级(如用户点击)
const InputContinuousLane = 0b00010; // 连续交互(如拖拽)
const DefaultLane = 0b00100; // 默认(如请求数据返回)
const IdleLane = 0b10000; // 最低(空闲时执行)
let pendingLanes = 0b00000; // 当前待处理的所有任务
// 1. 添加任务 (按位或)
pendingLanes |= SyncLane;
pendingLanes |= DefaultLane;
// 此时 pendingLanes 为 0b00101
// 2. 提取最高优先级任务 (查找最低位的 1)
// 算法公式:lanes & -lanes
const highestPriorityLane = pendingLanes & -pendingLanes;
// 0b00101 & 0b11011 = 0b00001 (成功提取出 SyncLane)
// 3. 移除已完成的任务 (按位与和按位取反)
pendingLanes &= ~highestPriorityLane;
// 0b00101 & 0b11110 = 0b00100 (剩下 DefaultLane)
4. Hooks 执行机制 (链表存储)
Hooks 赋予了函数组件状态,但函数每次渲染都会重新执行,React 是如何记住状态的?
答案是:每个 Fiber 节点上有一个 memoizedState 属性,它存放着一个由 Hooks 组成的单向链表。
这就是为什么 React 严格规定 “不要在循环、条件或嵌套函数中调用 Hook”——因为 React 完全依赖 Hooks 的执行顺序来将状态与对应的 Hook 匹配。
Mermaid 图表:Hooks 链表结构
代码模拟:Hooks 的顺序调用机制
JavaScript
let currentlyRenderingFiber = null;
let workInProgressHook = null;
function useState(initialState) {
// 1. 构建当前 Hook 节点
const hook = {
memoizedState: initialState, // 存储状态
next: null // 指向下一个 Hook
};
// 2. 将 Hook 挂载到 Fiber 节点的链表上
if (workInProgressHook === null) {
// 这是第一个 Hook
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 顺延到下一个 Hook
workInProgressHook = workInProgressHook.next = hook;
}
// 3. 返回状态和修改状态的函数
const dispatch = (newState) => {
hook.memoizedState = newState;
scheduleUpdateOnFiber(currentlyRenderingFiber); // 触发重新渲染
};
return [hook.memoizedState, dispatch];
}