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:
A → B → D → E → C通过指针推进:
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 phaseReact 结构是:
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 串在一起。
结构骨架:
- fiber.children:子节点
- fiber.sibling:兄弟节点
- 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中,从而实现批处理。