本节主要关注React初始渲染中Fiber树的构建过程,将忽略一些不相关内容。
调试用的源码如下,推荐实践调试。这里的例子很简单,但具备了Fiber树的结构要素:child、return与sibling。
源码调试可参考这篇文章:深入React(1)调试React源码
jsx
const container = document.getElementById('app');
debugger; // 从这里开始断点调试
const root = ReactDOM.createRoot(container);
root.render(
<div>
<h1>Hello World</h1>
<a href='https://react.dev'>Welcome to React</a>
</div>
);
总体的核心流程如下:
createRoot
下面截取了源码中的一些核心逻辑。
jsx
function createRoot(container){
var root = createContainer(container);
// ReactDOMRoot构造函数返回 {_internalRoot: root}
return new ReactDOMRoot(root);
}
createContainer是个套壳,里面只执行了createFiberRoot的调用。
jsx
function createFiberRoot(containerInfo){
var root = new FiberRootNode(containerInfo);
var uninitializedFiber = createHostRootFiber();
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
return root;
}
这里创建了一个FiberRootNode与HostRootFiber,并通过current与stateNode相互引用。HostRootFiber是整个Fiber树构建的起点 ,FiberRootNode的containerInfo属性记录了React绑定的container信息(一个DOM元素) 。

这里的tag
记录了FiberNode的类型,后面会提到,React会根据tag做不同的处理。
render
jsx
root.render(
<div>
<h1>Hello World</h1>
<a href='https://react.dev'>Welcome to React</a>
</div>
);
上述代码中的JSX会先经过Babel编译为下面的内容:
jsx
root.render(React.createElement(
'div',
null,
React.createElement(
'h1',
null,
'Hello World'
),
React.createElement(
'a',
{ href: 'https://react.dev' },
'Welcome to React'
)
));
可以理解为JSX就是ReactElement,它的内容如下。ReactElement中记录了元素标签、属性以及子元素信息,通过它可以还原出真实DOM。

render()
的核心逻辑如下。它将传入的ReactElement保存到update.payload中,然后插入一个异步的更新队列。
jsx
function updateContainer(){
var update = createUpdate(lane);
update.payload = {
element: element
};
var root = enqueueUpdate(current$1, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, current$1, lane);
entangleTransitions(root, current$1, lane);
}
return lane;
}
这里忽略什么时候开始进行更新,直接跳到正式的更新入口
performConcurrentWorkOnRoot
。调试时直接找到performConcurrentWorkOnRoot
函数,在该处打断点并跳转即可。
performConcurrentWorkOnRoot
虽然叫performConcurrentWorkOnRoot,但首次渲染会走renderRootSync()
,即同步渲染。
jsx
// shouldTimeSlice为false
shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);
renderRootSync的核心逻辑是workLoopSync()
,其源码如下:
jsx
function workLoopSync() {
// Perform work without checking if we need to yield between fiber.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
容易看出来这是一个同步迭代的过程。workInProgress
即当前正在处理的FiberNode ,它的初始状态为前面提到的HostRootFiber。
performUnitOfWork
performUnitOfWork的核心逻辑如下。这里重点关注beginWork
与completeUnitOfWrok
两个函数。
beginWork中会创建并返回下一个Fiber节点,如果存在,则继续处理返回的节点,反之则执行对当前节点执行completeUnitOfWork。
jsx
function performUnitOfWork(unitOfWork) {
var current = unitOfWork.alternate;
setCurrentFiber(unitOfWork);
// 初次渲染中,所以current与unitOfWork相同,暂时不用管这个逻辑
var next = beginWork(current, unitOfWork);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
beginWork
beginWork
会根据 workInProgress.tag
来做不同处理 。前面提到初始化时workInProgress为HostRootFiber,因此将首先执行updateHostRoot。当后续workInProgress更新为div等节点时,则将执行updateHostComponent$1。
jsx
switch (workInProgress.tag) {
// ...
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent$1(current, workInProgress, renderLanes);
// ...
}
这里需要注意下两者的区别:它们最终都会执行reconcileChildren(),并返回workInProgress.child ,不同的是children从何处获取。
jsx
// updateHostRoot
function updateHostRoot(){
// 对于hostRoot,会从前面提到的update.payload中取出element,作为hostRoot的children
// 并存储在workInProgress.memoizedState.element中
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
var nextState = workInProgress.memoizedState;
var nextChildren = nextState.element; // 即root.render()中传入的ReactElement
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
// updateHostComponent$1
function updateHostComponent$1(){
// 对于hostComponent,直接从workInProgress.pendingProps.children中获取子元素
var nextProps = workInProgress.pendingProps;
var nextChildren = nextProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
对于hostRoot,会从前面提到的update.payload中取出element,作为hostRoot的children。而对于普通元素,则可以直接从fiber节点的pendingProps中取出children。后面会提到普通元素的fiber节点是如何构造出来的。
beginWork中的重点就是reconcileChildren,它是构建Fiber树的关键逻辑。
reconcileChildren
reconcileChildren
的源码如下。
jsx
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.
// If we had any progressed work already, that is invalid at this point so
// let's throw it out.
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
}
}
mountChildFibers与reconcileChildFibers仅仅是一个参数的区别,这个参数名为shouldTrackSideEffects
,后面会提到它的作用所在。
jsx
var reconcileChildFibers = createChildReconciler(true); // shouldTrackSideEffects为true
var mountChildFibers = createChildReconciler(false); // shouldTrackSideEffects为false
mountChildFibers与reconcileChildFibers最终都会调用reconcileChildFibersImpl
。其中会根据子元素是单个元素还是数组来决定是否调用reconcileChildrenArray
。
jsx
function reconcileChildFibersImpl(returnFiber, currentFirstChild, newChild, lanes){
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
// 子节点是一个React Element,比如示例中的div
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
// ...
}
// 子节点是一个数组,比如示例中的div的子元素[h1, a]
if (isArray(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
}
}
}
比如hostRoot的子元素为div,则将执行placeSingleChild(reconcileSingleElement())
;而div的子元素为[h1, a],则将执行reconcileChildrenArray()
。
reconcileSingleElement
reconcileSingleElement的核心逻辑如下。它根据element创建一个新的Fiber节点,并将其return属性指向returnFiber。
jsx
var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);
_created4.ref = coerceRef(returnFiber, currentFirstChild, element);
_created4.return = returnFiber;
根据示例中的div元素创建的fiber结构如下:
jsx
var _created4 = {
type: 'div',
elementType: 'div',
pendingProps: {
children: [h1, a]
},
return: FiberNode, // HostRoot
child: null,
sibling: null,
flags: 0
}
它包含了element的类型与属性,并有一个return属性指向它的父节点。
同时执行的逻辑还有placeSingleChild
,其源码如下。设置fiber.flags属性为Placement意味着这个节点是需要插入的。
jsx
function placeSingleChild(newFiber) {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement | PlacementDEV;
}
return newFiber;
}
这里出现了shouldTrackSideEffects
,它的作用是什么呢?设想一下,对于<div><h1 /><a /></div>
这样一个结构,是否每个节点都要标记为"插入"呢?显然不是,我们只需要插入div这个根节点到页面上即可 。所以在初始化渲染过程中,只有hostRoot会调用reconcileChildFibers(shouldTrackSideEffects为true) ,即div节点会被标记为Placement。而其他子节点只会调用mountChildFibers(shouldTrackSideEffects为false),不会被标记(后面会看到<h1>
与<a>
会被添加为<div>
的的子节点,所以最后只插入<div>
就能显示全部内容)。
reconcileChildrenArray
这个函数里包含了子元素diff的逻辑,但是对于初始化渲染只需执行下面的部分代码。
jsx
if (oldFiber === null) {
// 遍历children
for (; newIdx < newChildren.length; newIdx++) {
// 根据element创建fiber,并设置return属性指向父节点
var _newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (_newFiber === null) {
continue;
}
// 示例中此时shouldTrackSideEffects为true,所以placeChild不会添加flags更新
lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = _newFiber;
} else {
// 设置sibling为下一个子节点
previousNewFiber.sibling = _newFiber;
}
previousNewFiber = _newFiber;
}
return resultingFirstChild;
}
这里创建子元素的fiber节点时,除了设置return指向父节点,还会设置sibling指向同级的下一个子节点,形成下面的结构。最后将第一个子节点返回作为下一个处理节点。

completeUnitOfWork
当beginWork()
的返回值为null,即当前fiber节点不存在子节点时,将执行 completeUnitOfWork
。比如示例中的h1与a元素,它们是不存在子节点的。
jsx
function performUnitOfWork(unitOfWork) {
// ...
var next = beginWork(current, unitOfWork, entangledRenderLanes);
// 示例中beginWork总是返回unitOfWork.child,如果不存在则执行completeUnitOfWork
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
completeUnitOfWork的核心逻辑如下,它也是执行一个循环逻辑。
- 首先处理当前节点
completeWork()
。 - 然后判断当前节点的sibling是否存在,存在则跳出循环,后续将执行performUnitOfWork(sibling) 。
- 不存在sibling则设置completedWork为returnFiber,继续执行completeUnitOfWork(returnFiber) 。
- 直到当前节点的returnFiber为空(最终是HostRootFiber,其return为空)。
jsx
function completeUnitOfWork(unitOfWork) {
var completedWork = unitOfWork;
var returnFiber = completedWork.return;
do {
next = completeWork(current, completedWork, entangledRenderLanes);
if (next !== null) {
workInProgress = next;
return;
}
var siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// 如果sibling存在则后续将执行performUnitOfWork(sibling)
workInProgress = siblingFiber;
return;
}
// 如果sibling不存在则执行completeUnitOfWork(returnFiber)
completedWork = returnFiber;
workInProgress = completedWork;
}while (completedWork !== null);
}
completeWork()
的处理逻辑如下,其主要作用是根据fiber结构创建出真实DOM 。如果存在子节点,则将子节点的DOM添加到当前节点的DOM上。但需要注意,这里虽然构建了真实DOM,但并未插入到页面上,因此此时页面上还不会显示出内容(这里照应了前面所说的shouldTrackSideEffects内容,最后只需将div这个根元素插入到页面上即可)。
jsx
function completeWork(current, workInProgress, renderLanes) {
var newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case HostComponent:
{
var _type2 = workInProgress.type;
// div#app
var _rootContainerInstance = getRootHostContainer();
// 调用document.createElement(type)创建DOM元素
var _instance3 = createInstance(_type2, newProps);
// 调用domNode.appendChild(child)添加子元素到当前的DOM元素中
appendAllChildren(_instance3, workInProgress);
// 将fiber的stateNode属性指向创建的真实DOM
workInProgress.stateNode = _instance3;
// 将props属性设置到DOM元素上
if (finalizeInitialChildren(_instance3, _type2, newProps)) {
markUpdate(workInProgress);
}
}
case HostRoot:
{
// 处理HostRootFiber
}
}
}
beginWork是从HostRootFiber开始,而最终completeUnitOfWork也是以HostRootFiber结束。至此整个workLoopSync循环就结束了。完整流程示意图如下:
整体而言,renderRootSync(workLoopSync)做了哪些事情呢?
- 针对root.render()传入的ReactElement同步构建了一棵完整的Fiber树,这些fiber节点通过return、child、sibling相连。
- 每个fiber节点都保存了对应的ReactElement的信息,通过
createElement
API创建出真实的DOM节点,并保存在fiber的stateNode属性上。div的子节点h1与a则通过appendChild
API被添加到子元素中。 - 只有div这个根元素的fiber节点被标记了Placement(插入)。
commitRoot
在前面的render过程中已经构建了一棵完整的Fiber树,并且进行了标记(flags)。最后还有一个commit的过程来将这些标记更新到页面上,成为我们可见的内容。
比如示例中只有div元素标记了Placement,最后将执行下面的代码做更新:
jsx
appendChildToContainer(parent, stateNode);
// 最终将调用appendChild API将div添加到container元素(div#app)上
parentNode.appendChild(child);
总结一下,这篇文章重点梳理了初始化渲染中有关Fiber树构建的部分内容,但很多React的核心内容还没有涉及到,可以把它当成是一个起点,后面慢慢拓展开来。