目标虚拟 DOM
js
const element = (
<h1>
hello <span>test</span> children
</h1>
);
针对以上代码的虚拟DOM就不在这里再列出来了。
架构分析
在 createRoot
函数中,已经实现了 HostRootFiber
和 FiberRootNode
。接下来需要开始构建 Fiber
树。先上图。

在上图中,出现了一些属性,这里需要着重介绍。
双缓存
在上图中,出现了被方框分隔的两部分,他们都是 Fiber 树。在源代码中,分别称为 current
和 workInProgress
。
current
: 正在屏幕上显示的 Fiber 树。workInProgress
: 正在构建、即将替换 current 的那棵树。
这是为了高效比较前后两次更新,并且可中断和批量一次性提交。
current
和 workInProgress
之间通过 alternate 属性互相指向,当 workInProgress
完成并且提交后,workInProgress
会成为 current
,current
会转为 workInProgress
,实现复用。
Fiber 树
Fiber 树的关键元素有这几个:
- child: 当前 Fiber 节点的大儿子
- return: 当前 Fiber 节点的父亲
- sibling: 当前 Fiber 节点的弟弟
Fiber 树是根据虚拟DOM 树构建而来。通过以上三个属性构建出 Fiber 树。
Fiber 树叫树是因为长得像树,但是实际的数据结构是链表,可以很方便的中断链表任务。
每个 Fiber 节点会被遍历两次,第一次根据虚拟DOM构建 Fiber 树,第二次完成构建。
代码实现
代码逻辑从 renderRootSync
函数开始看,忽略之前一些调用函数。
renderRootSync
function renderRootSync(root) {
// 开始构建 fiber 树
prepareFreshStack(root);
workLoopSync();
}
可以看到 renderRootSync
函数中调用了两个函数。 prepareFreshStack
和 workLoopSync
函数。
prepareFreshStack
function prepareFreshStack(root) {
// 创建 workInProgress
workInProgress = createWorkInProgress(root.current, null);
}
这里的 createWorkInProgress
的作用就是创建 workInProgress
,逻辑是当 current.alternate
不存在时创建一个新的 Fiber
,存在时则复用。
重点看 workLoopSync
函数。
workLoopSync
function workLoopSync() {
while (workInProgress) {
performUnitOfWork(workInProgress);
}
return workInProgress;
}
这个函数其实很简单,就是调用了 performUnitOfWork
函数。
performUnitOfWork
function performUnitOfWork(unitOfWork) {
// 获取新 fiber 对应的老 fiber
const current = unitOfWork.alternate;
// 完成当前 fiber 的子 fiber 链表构建后,返回 child
const next = beginWork(current, unitOfWork);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 没有子节点,则已经完成
completeUnitOfWork(unitOfWork);
} else {
// 存在子节点,则继续构建
workInProgress = next;
}
}
如果忽略 performUnitOfWork
中的 completeUnitOfWork
逻辑,那么这就是一个经典的链表遍历,通过 workInProgress
和 next
变量缓存 child 节点,一直到没有子节点退出。
beginWork
export function beginWork(current, workInProgress) {
switch (workInProgress.tag) {
// 处理根节点
case HostRoot:
return updateHostRoot(current, workInProgress);
// 处理原生节点
case HostComponent:
return updateHostComponent(current, workInProgress);
// 处理文本节点
case HostText:
return null;
default:
return null;
}
}
beginWork
中,对 Fiber 节点进行区别,不同类型的节点执行不同的处理函数。
updateHostRoot
function updateHostRoot(current, workInProgress) {
// 虚拟DOM信息在更新队列中
processUpdateQueue(workInProgress);
const nextState = workInProgress.memoizedState;
const nextChildren = nextState.element;
// 根据新的虚拟DOM 生成新的 fiber 链表
reconcileChildren(current, workInProgress, nextChildren);
return workInProgress.child;
}
在 updateHostRoot
函数中,首先要处理的是更新队列,这里需要知道,虚拟DOM 信息在一开始初始化更新队列的时候就放到了根节点的更新队列中,所以这个处理逻辑就很清楚,就是将更新队列中的所有更新合并,并返回,然后赋值到 workInProgress.memoizedState
。对于根节点来说,nextChildren
就是 memoizedState
中的 element
元素。再执行 reconcileChildren
函数,这个函数的作用是根据虚拟DOM,生成 Fiber 链表。最后返回 workInProgress.child
。
processUpdateQueue
const queue = workInProgress.updateQueue;
const pendingQueue = queue.shared.pending;
// 如果有更新
if (pendingQueue !== null) {
// 清除等待生效的更新
queue.shared.pending = null;
// 获取队列中最后一个更新
const lastPendingUpdate = pendingQueue;
// 获取队列中第一个更新
const firstPendingUpdate = lastPendingUpdate.next;
// 解开循环链表
lastPendingUpdate.next = null;
// 获取老状态
let newState = workInProgress.memoizedState;
let update = firstPendingUpdate;
while (update) {
// 根据老状态和更新,计算新状态
newState = getStateFromUpdate(update, newState);
update = update.next;
}
// 把最终计算的状态赋值
workInProgress.memoizedState = newState;
reconcileChildren
function reconcileChildren(current, workInProgress, nextChildren) {
// 如果新 fiber 没有老 fiber 则说明是挂载
if (current === null) {
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren);
} else {
// 如果有老老 fiber 则说明是更新
// DOM-DIFF 拿老 fiber 和新的虚拟DOM进行对比,进行更新
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren);
}
}
arduino
// 没有老 fiber,走这个方法挂载
export const mountChildFibers = createChildReconciler(false);
// 存在老 fiber,走这个方法更新
export const reconcileChildFibers = createChildReconciler(true);
对于 createChildReconciler
函数就不细看了,它的工作主要有以下几点:
- 生成子 Fiber 节点并挂载到父节点(单节点和多节点)
- 标记是否操作标识(flags) 在
createChildReconciler
函数执行完成后,返回子节点,如果子节点为空,则进入completeUnitOfWork
函数。
completeUnitOfWork
function completeUnitOfWork(unitOfWork) {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// 执行完成逻辑
completeWork(current, completedWork);
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}
completeUnitOfWork
函数是先遍历所有兄弟节点,如果没有兄弟节点,则回到父节点。是自底向上的遍历。具体代码也就不看了,注意功能就是根据不同类型的 Fiber 节点创建不同的真实DOM。并将操作标记向上冒泡。
结束
以上就是第一次挂载节点的执行流程。