K
Some thoughts of react
创建于:2026-03-16 14:25:31
|
更新于:2026-03-17 16:35:54

react-scheduler

既然是时间分片,那么一定是存在长耗时的任务。在 react 中长耗时的任务也就是自顶向下的 diff 过程。

于是 react 将整个 fiber-tree 通过链表的形式构建,而 fiber 就是其中最小的工作单元。

React 的调度可以理解为 Scheduler 调度 root render 任务,render 过程在 workLoop 中被拆成 fiber work unit 执行,通过时间片控制实现可中断

注意可中断的只有 遍历 fiber 链的过程,单个 fiber 的 render 是无法中断的。

整体结构可以按三层理解。

第一层:更新产生

setState
enqueueUpdate (加入 fiber.updateQueue)
 → scheduleUpdateOnFiber
 → ensureRootIsScheduled
 → Scheduler.scheduleCallback

这一步不会立即 diff,只是:

1)记录 update
2)确保 root 被调度

Scheduler 队列(小顶堆)里的任务其实是:

performConcurrentWorkOnRoot(root)

也就是 root render 任务

每次 set 都会产生 update 任务,任务带 lane(优先级、状态),该 lane 会上浮到 fiber.lanes、parent.fiber.lanes...root.lanes。

scheduler 会维护一个小顶堆,小顶堆是根据 lane 去计算 expirationTime,然后根据 expirationTime 进行排序,因此每次都会是优先级高的先执行。

第二层:root render 调度

Scheduler 通过宏任务触发执行:

performWorkUntilDeadline
 → flushWork
performConcurrentWorkOnRoot(root)

进入 render:

renderRootConcurrent
 → workLoopConcurrent

此时才真正开始 diff。

第三层:fiber work 拆分

workLoopConcurrent 会把 root diff 拆成一个个 fiber 单元:

while (workInProgress !== null && !shouldYield()) {
  performUnitOfWork(fiber)
}

每一次循环就是一个 fiber work unit

beginWork
reconcileChildren
completeWork

执行顺序类似 DFS:

ABDEC

通过指针推进:

workInProgress

时间切片机制

React 每执行一段时间就判断:

shouldYield()

如果时间片耗尽:

break

此时会保存:

workInProgress
workInProgressRoot

下一轮调度继续执行:

resume workLoop

因此 render 可以:

暂停 → 恢复

而不是重新从 root 开始 diff。

Scheduler 与 Fiber 的职责分工

Scheduler:

taskQueue (min heap)
  root render tasks

作用:

决定哪个 root render 先执行

Fiber:

workLoop
  fiber unit work

作用:

把 render 拆成可中断的执行单元

中断粒度

可中断:

fiber 之间

不可中断:

单个 fiber work unit

以及:

commit phase

React 结构是:

render phase  (可中断)
commit phase  (同步)

DOM mutation 一定是一次性完成。

例子

function App() {
  const [input, setInput] = useState('');
  const [list, setList] = useState([]);
 
  const onChange = e => {
    setInput(e.target.value);              // 高优(Sync/Input)
    startTransition(() => {
      setList(expensiveFilter(e.target.value)); // 低优(Transition)
    });
  };
 
  return (
    <>
      <input onChange={onChange} />
      <List data={list} />
    </>
  );
}

set -> update(lane) -> root -> scheduler -> root-render -> work-loop

大致流程是这样,set 会收集 update 到 hook.queue 中,并且将 lane 进行上浮直到 root 上,scheduler 维护的小顶堆会推一个 root-render 任务,再通过宏任务开启调度, 进行 work-loop,也就是遍历 fiber 进行 diff 的过程,遍历过程可中断,但 commit 过程不可中断。

因此 startTransition 产生的 root-render 会被中断,先去执行 setInput 的 root-render,也就是该案例会存在两次 re-render。

小顶堆根据 expirationTime 进行排序,但 expirationTime 会依据 lane 进行计算得到。

总结

最终可以把整个流程压缩成一条链路:

setState
 → enqueueUpdate
 → scheduleUpdateOnFiber
 → Scheduler.scheduleCallback
 → performConcurrentWorkOnRoot
 → renderRootConcurrent
 → workLoop
performUnitOfWork (fiber)
 → commit

一句话总结:

Scheduler 用小顶堆调度 root render;Fiber 把 render 拆成 unit work;workLoop + shouldYield + 指针保存实现可中断恢复。

fiber 架构

早起 react diff 的过程是同步递归,整个过程不可中断,可想而知嵌套层级比较深的结构,一定会阻塞的问题。

于是 fiber 架构应运而生,通过链表将整个 react-dom 串在一起。

结构骨架:

  1. fiber.children:子节点
  2. fiber.sibling:兄弟节点
  3. fiber.return:父节点

链表这种有向性的结构,天生就支持中断和恢复的。

hook 与 fiber

React Fiber 中的 Hook 本质是挂在 Fiber 节点上的单向链表结构,每个函数组件对应一条 Hook 链,通过顺序而不是 key 来定位。

这也是为什么不能在 if 判断中调用 hook 的原因,因为一单判断不成立,那么整个 hook 链都会对应不上。

type Hook = {
  memoizedState: any;     // 当前值(state / effect / memo 等)
  baseState: any;         // 用于跳过更新时的基础 state
  baseQueue: Update<any> | null; // 被跳过的更新队列
  queue: UpdateQueue<any> | null; // 更新队列(setState 就进这里)
  next: Hook | null;      // 指向下一个 hook(链表)
}
 
type UpdateQueue<S> = {
  pending: Update<S> | null; // 环状链表(最后一个节点)
  dispatch: (action) => void;
  lastRenderedReducer: (S, A) => S;
  lastRenderedState: S;
}
 
type Update<S> = {
  lane: Lane;           // 优先级
  action: any;          // setState 传入的值
  hasEagerState: boolean;
  eagerState: S;
  next: Update<S>;      // 环状链表
}

那么 setState 的批处理是如何实现的呢?

实际上在 多次 setState 的时候,会通过全局变量进行控制,只会触发一次 render,剩下的 update 都收集到hook.queue中,从而实现批处理。

我也是有底线的 🫠