面试造火箭,工作拧螺丝?今天我们把火箭拆了重装。当别人还在背“Fiber 是链表、时间切片是分片”时,我们将用 200 行核心代码,从
createRoot到useState,真实还原 React 18 的并发渲染(Concurrent Rendering)与时间切片(Time Slicing)底层机制。这篇文章不教你怎么用 API,而是带你写一个能跑起来的“微型 React”。
从 React 15 的递归调和(Reconciliation)到 React 16+ 的 Fiber 架构,本质是为了解决 “渲染不可中断” 的致命伤。React 18 引入的并发特性(startTransition、useDeferredValue)依赖更底层的 Lane 优先级模型。
但剥开层层源码,核心只有三件事:
我们从零开始,用原生 JS + 浏览器 API 实现这套机制。
Fiber 的本质是一个 单向链表树,每个节点存有 child(子节点)、sibling(兄弟节点)、return(父节点)。我们将类型简化为 HostRoot(根)、HostComponent(原生 DOM)、FunctionComponent(函数组件)。
// 工作优先级(简化版)
const NoLane = 0;
const SyncLane = 1; // 同步优先(类比 click 事件)
const DefaultLane = 2; // 普通优先级
const IdleLane = 3; // 空闲执行
// Fiber 节点定义
let nextUnitOfWork = null; // 当前待处理的 Fiber
let wipRoot = null; // 进行中根 Fiber(workInProgress)
let currentRoot = null; // 已提交的根 Fiber
let deletions = []; // 待删除节点
function createFiber(tag, key, pendingProps) {
return {
tag, // 'HostRoot' | 'HostComponent' | 'FunctionComponent'
key,
pendingProps, // 新传入的 props
memoizedState: null, // 对于函数组件,存 Hook 链表;对于 DOM 存节点
updateQueue: null, // 更新队列
stateNode: null, // 真实 DOM 或组件实例
// 树结构指针
return: null,
child: null,
sibling: null,
// 双缓冲复用
alternate: null, // 指向 current 树中的对应节点
flags: 0, // 副作用标记(Placement | Update | Deletion)
lanes: NoLane,
childLanes: NoLane,
};
}假设我们有以下 App 组件:
function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}createRoot 入口:
function createRoot(container) {
// 根 Fiber 节点
const rootFiber = createFiber('HostRoot', null, null);
const root = {
containerInfo: container,
current: rootFiber,
};
rootFiber.stateNode = root;
return { render: (element) => scheduleRoot(root, element) };
}
function scheduleRoot(root, element) {
// 构建初始 Fiber 树
const rootFiber = createFiber('HostRoot', null, null);
rootFiber.stateNode = root;
root.current = rootFiber;
// 将 JSX 元素作为根 Fiber 的 pendingProps
rootFiber.pendingProps = { children: element };
// 开启工作循环
wipRoot = rootFiber;
currentRoot = null;
nextUnitOfWork = wipRoot;
requestIdleCallback(workLoop);
}关键 reconcileChildren(调和子节点):我们这里采用 单链表 Diff(React 18 实际是多指针遍历,但核心逻辑一致)。
function reconcileChildren(currentFiber, newChildren) {
if (!newChildren) return;
// 简化:将 children 转为数组
const children = Array.isArray(newChildren) ? newChildren : [newChildren];
let prevSibling = null;
let oldFiber = currentFiber.alternate ? currentFiber.alternate.child : null;
for (let i = 0; i < children.length; i++) {
const element = children[i];
// 根据 element 创建新的 Fiber(此处忽略 key 复用逻辑,聚焦主流程)
const newFiber = createFiber(
typeof element.type === 'function' ? 'FunctionComponent' : 'HostComponent',
element.key || null,
{ ...element.props, children: element.props?.children }
);
newFiber.return = currentFiber;
if (i === 0) {
currentFiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
}
}React 18 的并发依靠 调度器(Scheduler),这里我们使用 MessageChannel + requestIdleCallback 模拟(真实场景用 MessageChannel 制造宏任务,避免 requestIdleCallback 的 50ms 限制)。
主循环:每次执行一个 Fiber 单元(performUnitOfWork),然后检查剩余时间。
const frameDeadline = 0;
let scheduledHostCallback = null;
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (typeof MessageChannel !== 'undefined') {
const channel = new MessageChannel();
channel.port1.onmessage = function() {
const timeRemaining = () => frameDeadline - performance.now();
if (scheduledHostCallback && scheduledHostCallback(timeRemaining) === true) {
channel.port2.postMessage(null);
}
};
channel.port2.postMessage(null);
}
}
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// 检查剩余时间,若小于 5ms 则让出主线程
shouldYield = deadline.timeRemaining() < 5;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot(); // 进入提交阶段
}
// 若还有任务,继续申请下一次调度
if (nextUnitOfWork) {
requestHostCallback(workLoop);
}
}performUnitOfWork 负责执行当前 Fiber 并返回下一个待执行 Fiber(深度优先遍历):
function performUnitOfWork(fiber) {
// beginWork:执行当前 Fiber,生成子 Fiber
beginWork(fiber);
if (fiber.child) {
return fiber.child; // 优先遍历子节点
}
// 没有子节点,找兄弟节点
let nextFiber = fiber;
while (nextFiber) {
completeUnitOfWork(nextFiber); // 完成当前节点(收集 DOM 操作)
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.return; // 向上回溯
}
return null;
}beginWork 根据类型处理(核心 diff 逻辑):
function beginWork(fiber) {
if (fiber.tag === 'HostRoot' || fiber.tag === 'HostComponent') {
// 处理原生 DOM 或根节点
const children = fiber.pendingProps?.children;
reconcileChildren(fiber, children);
} else if (fiber.tag === 'FunctionComponent') {
// 执行函数组件,得到 JSX
const hook = fiber.memoizedState;
const children = fiber.type(fiber.pendingProps);
reconcileChildren(fiber, children);
}
}completeUnitOfWork 负责创建 DOM 实例并挂载到 Fiber 上:
function completeUnitOfWork(fiber) {
if (fiber.tag === 'HostComponent') {
if (!fiber.stateNode) {
// 创建 DOM 节点
const dom = document.createElement(fiber.type);
// 设置属性(简化)
const props = fiber.pendingProps || {};
Object.keys(props).forEach(key => {
if (key !== 'children') dom[key] = props[key];
});
fiber.stateNode = dom;
}
// 将子 Fiber 的 DOM 挂载到当前节点下(便于提交)
let child = fiber.child;
while (child) {
if (child.stateNode) {
fiber.stateNode.appendChild(child.stateNode);
}
child = child.sibling;
}
}
}当 nextUnitOfWork 为空时,表明整棵 Fiber 树构建完成,进入 Commit。React 18 的 Commit 分三步:before mutation、mutation、layout。这里我们合并为关键两步:替换根 DOM + 更新 current 指针。
function commitRoot() {
// 将构建好的 wipRoot 的 DOM 挂载到真实容器
const container = wipRoot.stateNode.containerInfo;
const fiberRoot = wipRoot;
// 获取根 Fiber 下的第一个子节点 DOM
if (fiberRoot.child) {
const dom = fiberRoot.child.stateNode;
container.innerHTML = ''; // 清空容器(生产环境优化)
container.appendChild(dom);
}
// 双缓冲切换:currentRoot 指向旧树,wipRoot 变为 current
currentRoot = wipRoot;
wipRoot = null;
}Hooks 的执行依赖 当前 Fiber 上的 Hook 链表。我们以 useState 为例,完整还原 调度更新 机制。
let currentlyRenderingFiber = null;
let workInProgressHook = null;
function renderWithHooks(fiber) {
currentlyRenderingFiber = fiber;
fiber.memoizedState = null; // 重置 Hook 链表
workInProgressHook = null;
// 执行函数组件(内部触发 useState 等)
const children = fiber.type(fiber.pendingProps);
currentlyRenderingFiber = null;
return children;
}
function useState(initial) {
// 获取当前正在执行的 Fiber
const fiber = currentlyRenderingFiber;
// 获取当前 Hook 或创建新 Hook
let hook;
if (!workInProgressHook) {
// 第一个 Hook
hook = {
memoizedState: initial,
next: null,
queue: [], // 更新队列
};
fiber.memoizedState = hook;
workInProgressHook = hook;
} else {
// 复用 alternate 树中的旧 Hook
const alternateHook = fiber.alternate?.memoizedState;
// 遍历找到对应的 hook(这里简化单链表取 next)
if (workInProgressHook.next) {
hook = workInProgressHook.next;
} else {
hook = {
memoizedState: alternateHook?.memoizedState || initial,
next: null,
queue: [],
};
workInProgressHook.next = hook;
}
workInProgressHook = hook;
}
// 执行队列中的更新(计算新 state)
let newState = hook.memoizedState;
while (hook.queue.length) {
const action = hook.queue.shift();
newState = typeof action === 'function' ? action(newState) : action;
}
hook.memoizedState = newState;
// 返回 [state, setState]
const setState = (action) => {
// 将更新加入队列
hook.queue.push(action);
// 触发一次新的调度(重新渲染)
scheduleUpdate(fiber);
};
return [newState, setState];
}
function scheduleUpdate(fiber) {
// 从当前 Fiber 向上回溯,设置 wipRoot
wipRoot = fiber;
// 构建 alternate(复用 current 树结构)
wipRoot.alternate = currentRoot;
nextUnitOfWork = wipRoot;
requestIdleCallback(workLoop);
}useEffect 简易实现(副作用收集与执行):
function useEffect(callback, deps) {
const fiber = currentlyRenderingFiber;
const hook = mountWorkInProgressHook(); // 同上链表逻辑
const prevDeps = hook.memoizedState?.deps;
// 比较依赖是否变化(浅比较)
const hasChanged = !prevDeps || deps.some((dep, i) => dep !== prevDeps[i]);
if (hasChanged) {
hook.memoizedState = { deps, callback };
// 标记副作用(在 commit 阶段执行)
fiber.flags |= 1; // PassiveEffect
}
}React 18 支持 startTransition,本质是标记低优先级更新。我们通过 优先级队列 模拟:
let rootLanes = 0;
function scheduleUpdate(fiber, lane = DefaultLane) {
// 标记 Fiber 及其祖先的 lanes
let node = fiber;
while (node) {
node.lanes |= lane;
node = node.return;
}
// 根节点累加
if (wipRoot) {
wipRoot.lanes |= lane;
}
// 若遇到 SyncLane,立即执行(不切片)
if (lane === SyncLane) {
performSyncWork();
} else {
requestIdleCallback(workLoop);
}
}
function performSyncWork() {
// 同步执行所有 Fiber,不检查时间片
while (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (wipRoot) commitRoot();
}在 workLoop 中,我们优先处理高 Lane 的节点:
function getNextLane(root) {
if (root.lanes & SyncLane) return SyncLane;
if (root.lanes & DefaultLane) return DefaultLane;
return IdleLane;
}将上述代码整合,运行:
<body>
<div id="root"></div>
<script>
// 定义 App 组件(全局)
function App() {
const [count, setCount] = useState(0);
return {
type: 'div',
props: {
children: [
{ type: 'h1', props: { children: count } },
{ type: 'button', props: { onClick: () => setCount(c => c + 1), children: '+1' } }
]
}
};
}
const root = createRoot(document.getElementById('root'));
root.render({ type: App, props: {} });
</script>
</body>点击按钮时,setCount 触发 scheduleUpdate,浏览器在空闲时间执行 workLoop,界面无卡顿更新——这就是时间切片赋予 React 18 的“并发”超能力。
真实 React 18 特性 | 我们的简化版实现要点 |
|---|---|
双缓冲复用 | 通过 alternate 复用旧 Fiber,我们已在 useState 中实现 alternateHook 取值 |
Batched Updates(批量更新) | 真实源码通过 executionContext 控制,我们可模拟 isBatching 标志,将多个 setState 合并到一次调度 |
Suspense & 流式 SSR | 需在 Fiber 中增加 Suspense 类型,并暂停 completeUnitOfWork,属于上层扩展 |
原生事件系统 | 真实 React 采用合成事件,我们直接绑定 onClick 到 DOM,不影响主逻辑理解 |
性能关键:在 reconcileChildren 中,如果直接 innerHTML='' 会丢失内部状态。生产级应通过 flags 标记 精准增删改(Placement、Update、Deletion)。优化提示:在 completeUnitOfWork 中,若 fiber.flags & Placement 才执行 appendChild。
我们用不到 300 行代码,真实模拟了 React 18 从 JSX 解析 → Fiber 构建 → 时间切片调度 → 双缓冲提交 → Hooks 更新驱动 的全链路。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。