React的Fiber可中断是生成器函数实现的吗?
发布时间: 2026-01-06
前言
写React的朋友多少都知道Fiber架构,诸如Fiber如何优秀,可中断,分片执行,提高React运行性能,于是我就在想React的中断能力是否就是
生成器函数function*来实现的,带着这个疑惑我又去翻了一下Fiber任务调度的源码,来寻求解开我心中的答案。
在继续之前,可能需要你了解Fiber树的结构,在之前的文章中我有提到过 从React Diff算法看如何写出高性能的组件 ,如果你不清楚什么是Fiber以及什么是 链表结构,可以先看看之前的文章。
为什么要中断?
这主要是js单线程设计带来的问题,current树和workInProgress树,如果不中断,两棵树的对比工作一开始就不会停下来,这可能是一个非常耗时的过程,一直不释放主线程, 那么用户就会直接体验到鼠标点不了页面,页面也卡主不动了,所有的按钮、输入组件都不再响应用户的输入,所以要适当时间主动让出主线程就变得非常有必要。
这里说个题外话,尽管这些年浏览器发展出了一些新解决方案,比如像worker来启动新线程,但是我觉得阻塞主线程的问题仍然没有得到一个重视,苦了一众程序员,通过各种 算法、宏任务、微任务等骚操作去避免阻塞主线程。为什么一直不允许多线程?说什么增加复杂性?bull shit,现在服务端容器化之后,多实例也玩得照样溜,有同步问题解决同步问题就好了。
可中断的原理
这里Fiber的中断是指的调和阶段,不是提交,提交之后是不能暂停的,调和阶段主要的工作就是对比双缓冲树中的两棵树的差异。
看中断调度的关键代码,就2个函数。
workLoopConcurrentByScheduler()
// 有一个全局变量workInProgress就是当前处理的Fiber节点
// The fiber we're working on
let workInProgress: Fiber | null = null;
function workLoopConcurrentByScheduler() {
// 只要节点存在,且不需要让出主线程,那么就调用performUnitOfWork处理这个节点的比对计算工作
while (workInProgress !== null && !shouldYield()) {
// $FlowFixMe[incompatible-call] flow doesn't know that shouldYield() is side-effect free
performUnitOfWork(workInProgress);
}
// 如果执行到这里,是因为:
// 1. 所有节点都比对计算完了 (workInProgress === null) -> 本次 Render 完成
// 2. 时间片用完了 (shouldYield() 返回 true) -> 暂停,让出主线程,等待 Scheduler 下次调度
// 这里因为有个全局变量记录当前处理的节点是谁,加上链表只需要一个节点就
// 能找到它的父节点和下一个子节点,所以下次执行就能从上次执行的位置开始执行
}shouldYieldToHost()
这里的shouldYieldToHost就是上面调用的shouldYield函数,也就是workLoopConcurrentByScheduler函数执行超过5ms就会暂停,让出主线程一次,等到主线程空闲
之后再调用workLoopConcurrentByScheduler。
// export const frameYieldMs = 5;来自packages/scheduler/src/SchedulerFeatureFlags.js
let frameInterval: number = frameYieldMs;
function shouldYieldToHost(): boolean {
// 如果有紧急的绘制需求 (needsPaint),且配置允许,则立即暂停
if (!enableAlwaysYieldScheduler && enableRequestPaint && needsPaint) {
// Yield now.
return true;
}
// 检查当前任务执行时间是否超过了时间片 (frameInterval, 5ms)
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
// The main thread has only been blocked for a really short amount of time;
// smaller than a single frame. Don't yield yet.
// 还没超时,继续干活
return false;
}
// Yield now.
// 超时了,暂停执行,让出主线程
return true;
}从上面2个函数就已经看清楚为什么Fiber可以中断了,大白话就是Fiber节点一个一个的执行workLoopConcurrentByScheduler中的performUnitOfWork函数,需要中断的时候就return,下次检查的时候
发现上次任务没全部执行完,又调用workLoopConcurrentByScheduler中的performUnitOfWork函数,因为使用了一个变量workInProgress,所以还能接着上次执行的
位置继续执行。
performUnitOfWork是对一个Fiber节点进行计算处理
我对这个函数内的dev相关的代码删除,保留了核心的逻辑,可以清楚的看到Fiber树的处理是深度优先, 先处理当前节点(beginWork),完了处理子节点(beginWork返回子节点),如果没有子节点又处理兄弟节点 (completeUnitOfWork内部先给workInProgress设置为sibling), 都没有的话就回退到父节点(completeUnitOfWork内部没有sibling时,会把workInProgress设置为return父节点)。
// 这个函数会对Fiber挨个处理,处理完当前节点处理child,直到没有child
// 之后,又处理sibling节点,都处理完之后就回到return父节点
function performUnitOfWork(unitOfWork: Fiber): void {
// 从双缓冲拿当前渲染的dom节点出来,用于做对比
const current = unitOfWork.alternate;
let next;
// beginWork 返回该 Fiber 的第一个子节点 (child)
// 如果返回 null,说明这个分支遍历到底了(也就是没有子节点了)
next = beginWork(current, unitOfWork, entangledRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
// 如果没有子节点 (next === null)
// completeUnitOfWork 会:
// 1. 调用 completeWork(生成 DOM 节点、处理 Props 等)。
// 2. 检查是否有兄弟节点(sibling)。
// - 如果有,把 workInProgress 指向兄弟节点。
// - 如果没有,回到父节点,继续执行父节点的 completeWork
completeUnitOfWork(unitOfWork);
} else {
// 如果有子节点,把 workInProgress 指向子节点,
// 下次循环 performUnitOfWork 就会处理这个子节点。
workInProgress = next;
}
}React为什么不用生成器函数?
看了之后和我想的很不一样,并没有使用生成器函数,很明显React的这个设计已经足够简单,链表结构来让任务随时中断/恢复,一个简单的函数判断执行时长是否超出 5ms,就是这么简单就实现了任务的可中断能力,也不需要babel来支持老浏览器生成器函数的运行,终究是top级公司的代码。
