Skip to Content
全部文章实践出真知React的Fiber可中断是生成器函数实现的吗?

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()

packages/react-reconciler/src/ReactFiberWorkLoop.js
// 有一个全局变量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

packages/scheduler/src/forks/Scheduler.js
// 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级公司的代码。

最后编辑于

hi