在完成 Fiber 树的渲染后,需要提交 Fiber 树,将 Fiber 树对应的真实DOM渲染到页面上,即挂载到根节点上。
实现
performConcurrentWorkOnRoot
function performConcurrentWorkOnRoot(root) {
// 以同步方式渲染,第一次渲染都是同步
renderRootSync(root);
// 开始进入提交阶段,就是执行副作用,修改真实DOM
const finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
commitRoot(root);
}
在 renderRootSync
函数执行完成后,Fiber 树的处理也就完成了,这个时候有两个 Fiber 树,一个是只有一个根 Fiber 的空树,另一个是根据虚拟DOM渲染出的 Fiber 树。
commitRoot
function commitRoot(root) {
const { finishedWork } = root;
// 判断当前节点的子树是否有副作用
const subtreeHasEffects = (finishedWork.subtreeFlags & MutationMask) !== NoFlags;
// 根节点是否有副作用
const rootHasEffects = (finishedWork.flags & MutationMask) !== NoFlags;
// 有副作用则提交
if (subtreeHasEffects | rootHasEffects) {
commitMutationEffectsOnFiber(finishedWork, root);
}
// 等 DOM 变更后,指向完成的 Fiber 树
root.current = finishedWork;
}
ini
export const MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility;
在真正执行提交逻辑之前,需要判断当前 Fiber 节点或者当前 Fiber 节点的子 Fiber 是否存在副作用,有副作用再执行 commitMutationEffectsOnFiber
函数。MutationMask
是多种副作用的或计算。 改变 FiberRootNode
的 current
的指向,让它指向提交完成后的 Fiber 树,即 finishedWork
。
commitMutationEffectsOnFiber
// 暂时只处理原生节点
export function commitMutationEffectsOnFiber(finishedWork, root) {
switch (finishedWork.tag) {
case HostRoot:
case HostComponent:
case HostText: {
// 递归遍历,先处理子节点的副作用
recursivelyTraverseMutationEffects(finishedWork, root);
// 再处理自己的副作用
commitReconciliationEffects(finishedWork);
}
}
}
function recursivelyTraverseMutationEffects(parentFiber, root) {
if (parentFiber.subtreeFlags & MutationMask) {
let child = parentFiber.child;
while (child) {
commitMutationEffectsOnFiber(child, root);
child = child.sibling;
}
}
}
可以看到,对子节点的处理其实就是一个递归遍历,找到最下面的子节点,然后调用 commitReconciliationEffects
函数进行处理。
commitReconciliationEffects
function commitReconciliationEffects(finishedWork) {
const { flags } = finishedWork;
if (flags & Placement) {
// 插入
commitPlacement(finishedWork);
// 删除 Placement
finishedWork.flags = flags & ~Placement;
}
}
这里的逻辑页非常简单,判断副作用类型为插入,然后调用插入函数 commitPlacement
最后删除副作用标记。
commitPlacement 实现
对于插入操作,再根据 Fiber 节点的定义,可以很容易的想到。通过当前 Fiber节点的 return 属性获取父 Fiber 节点,在根据 stateNode 属性获取父 Fiber 节点对应的真实DOM,最后一插入,就搞定了。大概像这样。
js
function commitPlacement(finishedWork) {
const parentFiber = finishedWork.return
switch (parentFiber.tag) {
case HostRoot: {
const parent = parentFiber.stateNode.containerInfo;
parent.appendChild(parent, finishedWork.stateNode)
break;
}
case HostComponent: {
const parent = parentFiber.stateNode;
// 同上
}
}
}
但是对于下面图里这个 Fiber 树结构来说,这就不适用了。

通过这个图可以发现,父 Fiber 有可能是一个函数式组件,并没有对应的真实DOM。所以第一步,需要找到当前 Fiber 节点对应的真实DOM 节点。
js
function getHostParentFiber(fiber) {
let parent = fiber.return;
while (parent) {
if (isHostParent(parent)) {
return parent;
}
parent = parent.return;
}
return parent;
}
这个代码很简单,通过 while
循环一直往上找,直到父 Fiber 是一个真实DOM类型的 Fiber节点,或者不存在。 在找到真实父 Fiber 节点后,其实还是不可以直接插入,使用 append 会直接将子节点插入到父节点的最后,如果父节点的子节点不为空,那么会造成顺序的混乱,所以还需要找到对应的真实兄弟节点。 这里有几个点需要注意:
- 这个真实节点的副作用标识一定是更新。因为只有更新才代表这个节点在之前已经存在。如果是插入,表示这个节点还没有创建,不能插入。
- 如果当前节点没有兄弟,且父元素不为真实DOM类型 Fiber 节点,那么可以查看父节点的兄弟节点,不断循环,如果父节点为真实DOM 类型,则退出循环,直接插入。

图中蓝色箭头为执行顺序。
- 首先判断 Host1 是否存在兄弟节点,不存在则找父亲节点。
- 一直向上找,直到找到节点存在兄弟节点。
- 判断兄弟节点子节点,找到操作类型为更新的节点,返回。
js
function getHostSibling(fiber) {
let node = fiber;
sibling: while (true) {
while (node.sibling === null) {
if (node.return === null || isHostParent(node.return)) {
return null;
}
node = node.return;
}
node = node.sibling;
while (node.tag !== HostComponent && node.tag !== HostText) {
if (node.flags & Placement) {
continue sibling;
} else {
node = node.child;
}
}
if (!(node.flags & Placement)) {
return node.stateNode;
}
}
}
完整的 commitPlacement
函数。
js
function commitPlacement(finishedWork) {
const parentFiber = getHostParentFiber(finishedWork);
switch (parentFiber.tag) {
case HostRoot: {
const parent = parentFiber.stateNode.containerInfo;
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNode(parent, before, finishedWork);
break;
}
case HostComponent: {
const parent = parentFiber.stateNode;
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNode(parent, before, finishedWork);
break;
}
}
}