React 底层架构
前言
React 在版本 15 与 16 有比较大的更改,那么这么大费周章的重构一定有存在了以前架构无法解决的痛点了。
现代的前端开发框架之间竞争非常激烈,React 想要稳固他的地位,那么提升 React 的性能是躲不过的事情。
一、16、17、18 版本更新了什么?
- 16.0.0 更改底层架构,采用 Fiber,提出异步渲染
- 16.1.0 推出了实验性的 react-reconciler 包提供用户自定义 renderers
- ... 迭代更新
- 16.6.0 推出了实验性的 Scheduler
- 16.7.0 更新 Scheduler,正常迭代...
- 16.8.0 正式推出 Hook
- ... 迭代更新
- 16.13.0 实验性的 Concurrent Mode,
ReactDOM.createRoot() - 17.0.0 垫脚石版本,鼓励用户渐进式升级
- 改变了 React 事件委托对象,以前是 document 现改为 React 容器根节点
- 移除了事件池
- 异步执行
useEffect的 cleanup
- 18.0.0 主要是正式上线了 Concurrent Mode(并发模式)
- 自动批处理—以前只有 React 的事件才会批处理,而被
setTimeout等原生 js 语法包裹的事件无法批处理。 - 过渡更新—推出
startTransition告诉 React 哪些需要进行过渡更新,而不是紧急更新 - Suspense 更加方便
- 新的 Hook:
useId、useTransition、useDeferredValue、useSyncExternalStore、useInsertionEffect
- 自动批处理—以前只有 React 的事件才会批处理,而被
⚠️注意:React 默认不会开启 Concurrent Mode
二、为什么 React 要更新底层架构?
要想搞清楚这个问题,那么必须知道 React 是如何将元素渲染到页面中的(包括如何更新)。
就从 15 版本看:
- Reconciler—构建VDOM,准确来说 15 版本用的是
stack-reconciler - Rerender—渲染
以上两个便是 React 内部工作的核心,它们一个负责处理逻辑,一个负责渲染,各司其职,很美妙对不对。
但是,这是因为这样简单的结构导致 React 面对庞大数据量以及复杂交互时显得手忙脚乱(掉帧)。我们从一个简单的例子看起:
// 想象一个简单的列表渲染以及更新
// [1, 2, 3]
// 渲染:
// reconciler 发现 1 并且知道了它应该渲染成什么样子,一切处理好后告诉 rerender 该渲染了,rerender 乖乖执行渲染
// reconciler 处理 2,rerender 渲染
// reconciler 处理 3,rerender 渲染
// 一切看起来不错~
// 这时候,用户下发了乘法指令,所有数字乘以 2,此时变为了 [2, 4, 6]
// 更新:
// reconciler 发现原本为 1 的元素变为了 2,和之前不同,他兢兢业业处理好一切后告诉 rerender 该渲染了,rerender 乖乖执行渲染
// reconciler 处理 4,rerender 渲染
// reconciler 处理 6,rerender 渲染
上面的例子我们看起来也没什么问题,逻辑简洁清晰,但是如果用户的操作变多、数据量变大那么会出现什么情况呢?
tips:浏览器是一帧一帧的渲染,即每一帧会处理 JS 逻辑、样式布局、样式绘制。
我们知道一般浏览器的刷新频率为 60Hz,即每(1000ms / 60Hz)16.6ms 浏览器刷新一次。
也就是说我们在这 16.6ms 中完成上述的一系列操作(JS 逻辑处理、样式布局、样式绘制),用户并不会有任何不适(除非你的眼睛和普通人不同)。
但是,由于数据量增大,我们的处理逻辑(JS 逻辑)占用了大量时间导致超过了 16.6ms,这时也就让用户感受到了不适。
虽然这时候的架构会导致掉帧,那我们可以进行优化呀,emmm,的确如此,我们做开发首先想到的是如何优化原有逻辑。
那么我们来尝试优化下:
- reconciler 长时间执行会导致掉帧,那我们就把他改为可以中断的执行,每一帧留给 JS 执行一定时间去执行,如果执行不完就放到下一帧去完成
- rerender 逻辑不太好,我们优化为一次性更新,由 reconciler 全部处理完毕后统一渲染,这样也不会导致由于被中断的执行导致渲染不一致
嗯,上面的优化很合理,但是实际上我们的 reconciler 采用的是递归的方式执行,我们都知道递归一旦运行就无法暂停(即便可以退出递归,但是无法保证下次继续执行)。
直到此,我们无计可施,于是 React 决定重构底层架构。
三、Fiber 架构
从上一节我们知道,原本的 stack-reconciler 无法解决 React 高性能优化的需求;所以,React 推出了 fiber-reconciler,并且他的主要目标就是:
- 能够中断任务且把任务切片化执行
- 能够调整优先级,重置并复用任务
- 能够在父元素与子元素之间交错处理,以支持 React 中的布局
- 能够在
render()中返回多个元素(return [<Comp1/>, <Comp2/>]) - 更好地支持错误边界
那我们知道了新架构的目标,对于新架构的变化就更加清晰了,接下来我就来梳理下 Fiber 架构有哪些内容。
先一起来看看 Fiber 的结构:
/**
* HTML nodeType values that represent the type of the node
*/
var ELEMENT_NODE = 1;
var TEXT_NODE = 3;
var COMMENT_NODE = 8;
var DOCUMENT_NODE = 9;
var DOCUMENT_TYPE_NODE = 10;
var DOCUMENT_FRAGMENT_NODE = 11;
// WorkTag
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;
export const HostHoistable = 26;
export const HostSingleton = 27;
function FiberNode(
this: $FlowFixMe,
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
/** 作为静态数据结构的属性 */
// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同 type,某些情况不同,比如 FunctionComponent 使用 React.memo 包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于 ClassComponent,指 class,对于 HostComponent,指 DOM 节点 tagName
this.type = null;
// Fiber对应的真实 DOM 节点
this.stateNode = null;
/** 用于连接其他 Fiber 节点形成 Fiber 树(链表结构)*/
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
this.index = 0;
this.ref = null;
this.refCleanup = null;
/** 作为动态的工作单元的属性 */
// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// 保存本次更新会造成的DOM操作
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该 fiber 在另一次更新时对应的 fiber
this.alternate = null;
}
从上面 Fiber 的结构分析来对应如何实现上面所述的目标:
- 能够中断任务且把任务切片化执行 => 用于连接其他 Fiber 节点形成 Fiber 树(链表结构)
- 能够调整优先级,重置并复用任务 => 调度优先级相关
- 能够在父元素与子元素之间交错处理,以支持 React 中的布局 => 得益于渲染方式的改变以及可中断的任务
- 能够在
render()中返回多个元素(return [<Comp1/>, <Comp2/>]) => 数据结构的改变(sibling字段)
Fiber 节点之间的连接
function App() {
return (
<div>
<p>你好</p>
<div>
<p>senmu</p>
</div>
</div>
)
}
它们解析为 Fiber 结构,如图:

完整的流程
初始化渲染:
走
legacyRenderSubtreeIntoContainer方法,如果没有缓存的 root 容器,使用legacyCreateRootFromDOMContainer方法创建 root 容器- 会设置优先级为最高
- 初始化更新队列
updateQueue
走
updateContainer方法,加入更新队列,设置好优先级payload是element
flushSync(function () { updateContainer(initialChildren, _root, parentComponent, callback); });走
scheduleUpdateOnFiber将更新回调(performSyncWorkOnRoot方法)加入调度队列ensureRootIsScheduled该方法是重点,保证 root 容器可以被调度
走
flushSyncCallbacks执行同步调度队列(优先级最高的)走
performSyncWorkOnRootvar exitStatus = renderRootSync(root, lanes);需要拿到退出的状态
走
renderRootSync同步进行render阶段,拿到退出状态- 期间会走
prepareFreshStack创建工作进程(workInProgress) - 完成更新队列,并重新赋值下次更新队列
- 期间会走
走
workLoopSync,该工作是render阶段的重点,会遍历一边 Fiber 链表- 顺着上面构建的
workInProgress来进行遍历
- 顺着上面构建的
走
performUnitOfWork方法beginWork深度优先,完成第一个最深节点后进行completeUnitOfWork工作completeUnitOfWork完成工作,遇到 sibling 需要继续返回beginWork工作
commit阶段
beginWork 做了什么?
调用 beginWork 时需要传入 current、workInProgress、renderLanes 这三个参数
current当前的 Fiber 节点,其实就是workInProgress.alternate内容,也就是说在update时会复用的节点workInProgress很熟悉了,这个就是目前正在进行中的 FiberrenderLanes优先级相关
我们就初次渲染来看,current 不为空,那么就进入到了 update 逻辑,因为是初次渲染,也没有 props 与 context 的变化,一顿判断后来到关键逻辑,判断 Fiber 的类型, 根节点的类型为 HostRoot 也就是 3,就来到了 updateHostRoot 逻辑。
updateHostRoot 逻辑:
pushHostRootContext(workInProgress)建立维护 Context 与根容器(Container)的栈结构关系(包括 Protals 节点)- Context 通过维护栈来管理 Fiber 复杂的父子关系值
reconcileChildren内部同样有mount与update逻辑,但是它们内部方法一样- 协调算法,判断该节点是否可以复用或者要被删除等(diff)
- 标记插入、移动、删除、更新,便于后续渲染节点的操作
构建好了下一个节点返回执行栈
总结下 beginWork 的逻辑:
- 判断是否首次渲染
- 更新
- 判断是否可以复用
- 不可以复用的话跳出当前判断逻辑继续执行
- 可以复用
- 判断子节点是否需要更新
- 不需要的话就跳过
- 需要的话就返回该节点
- 判断是否可以复用
- 首次渲染
- 需要构建节点
- 判断节点的类型(
workInProgress.tag)- 根据节点类型不同会执行对应逻辑,上面的
updateHostRoot就是其中一个 reconcileChildren对应mountChildFibers(workInProgress, null, nextChildren, renderLanes);和reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);渲染或者更新,参考上面的逻辑
- 根据节点类型不同会执行对应逻辑,上面的
- 构建好下一个
workInProgress
流程图如下:

completeUnitOfWork 做了什么?
- 先判断是否有更紧急任务(
workInProgress.flag为Incomplete),如果没有进入当前节点的“completeWork(完成工作)”,如果有则要跳过 completeWork阶段- 同样判断节点的类型(
workInProgress.tag) - 判断是 update 还是 mount,进入不同逻辑
- 如果是 mount 的话,则会创建实例,并且将节点插入构成 Fiber 树,等待
commit阶段渲染为真实 DOM
var _rootContainerInstance = getRootHostContainer(); var instance = createInstance(_type, newProps, _rootContainerInstance, _currentHostContext, workInProgress); appendAllChildren(instance, workInProgress, false, false); workInProgress.stateNode = instance;- 如果是 update 的话,则会生成
updatePayload,并且赋值workInProgress.updateQueue = updatePayload(这部分是处理onClick、style prop、DANGEROUSLY_SET_INNER_HTML prop、children prop) - 最后
bubbleProperties(workInProgress)
- 同样判断节点的类型(
- 判断当前节点是否存在 sibling 节点,如果有的话要继续进入 sibling 节点的
beginWork工作
流程图如下:

commit 阶段做了什么?
rootDoesHavePassiveEffects存在就执行flushPassiveEffects()- 异步执行 Effect,其实就是异步执行
useEffect
- 异步执行 Effect,其实就是异步执行
commitBeforeMutationEffects()before mutation 阶段- 处理 autofocus
- 根据节点类型不同分别处理不同情况
- ClassComponent 会触发
getSnapshotBeforeUpdate()生命周期 - HostRoot 会清空容器内容(
container.textContent = "")
commitMutationEffects()mutation 阶段,主要是渲染真实 DOM- 递归处理节点(从父到子/自上而下)
- 先处理 Deletion
- 再处理 Placement
- 最后处理 Update
commitLayoutEffects()layout 阶段,执行 layout EffectonCommitRoot()给 React DevTools 等开发工具提供钩子,以便在完成渲染后做额外的处理,比如:性能监控、日志记录等
疑问:
- 如何构建
workInProgress===> 调用createWorkInProgress()方法
workInProgress.alternate = current;缓存该节点(第一次渲染时会缓存根节点)
- 谁去调用
workLoopSync,为什么会执行两次 ===> 错❌,并没有调用两次;只是内部执行var children = Component(props, secondArg);。 alternate字段什么含义?相当于节点的缓存 ✅beginWork()做了什么?✅- 初次渲染时进入
beginWork会进入update逻辑,这是因为在创建workInProgress时缓存了根节点 reconcileChildren()做了什么?(协调算法,初次渲染直接 mount 全部,更新时需要研究 diff 算法)rootDoesHavePassiveEffects这个变量什么时候赋值的?执行 Effect 之前会判断是否有正在进行的 EffectonCommitRoot()做了什么?✅
收获点:
- Google Closure Compiler -
/** @noinline */对应可以让该函数不被内联结构
loop1: do {
switch(name) {
case 'senmu':
// do something
break loop1; // 可以直接退出 do...while 循环
case 'SenMu':
// do something
break; // 只能退出 switch 语句
}
break; // 退出 do...while 循环
} while(true)
- React18 初始化渲染时不会开启切片/并发模式(不论是否用 createRoot 方法渲染),更新时需要用到某些 API 才能开启并发模式,例如:
startTransition
import { startTransition } from 'react';
function App() {
const [len, setLen] = useState(0);
// 模拟加载数据后更新列表
function loadData() {
startTransition(() => {
setLen(3000);
});
}
return (
<div>
<button onClick={loadData}>Load Data</button>
<ul>
{Array(len)
.fill(0)
.map((_, i) => (
<li key={i}>{i}</li>
))}
</ul>
</div>
);
}
