写在专栏开头(叠甲)
-
作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。
-
本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。
-
本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。
本一节的内容
本节的我们将从 上一节留下的问题出发,谈谈 render() 中的 updateContainer 做了什么工作,他怎么样把 传入进去的 DOM 元素变成我们的 Fiber 元素。因为这部分涉及到大量进程调度的问题,我们暂时不谈论这些问题,只是简单谈谈从 updateContainer 开始到最后变成 Fiber 经历的一系列的函数链,以及他们分别做了什么工作,进程调度的全部内容我们会在之后详细谈谈
updateContainer 的调用
上一节中,我们对 render 的流程讲解停在这里,我们讲到我们把挂载的节点和 FiberRootNode 放到了 updateContainer 中,这个函数负责生成 Fiber 的逻辑
ReactDOMRoot.prototype.render = function (children) {
const root = this._internalRoot;
if(root === null) {
throw new Error('Cannot update an unmounted root.');
}
// 渲染
updateContainer(children, root, null, null);
}
那么我们来看看 updateContainer 做了什么,他的源码在这个位置:
https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberReconciler.js
我们大概来看看,这里省略部分 dev 模式代码
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// 获取 FiberRootNode 的根节点
const current = container.current;
//创建一个更新
const eventTime = requestEventTime();
const lane = requestUpdateLane(current);
const update = createUpdate(eventTime, lane);
update.payload = { element };
// 处理 callback,不过从React18开始,render不再传入callback了,即这里的if就不会再执行了
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
//把创建的添加到 current 的更新链表中
enqueueUpdate(current, update, lane);
//根据优先级和创建一个更新任务,取出 element 构造下一个 Fiber
const root = scheduleUpdateOnFiber(current, lane, eventTime);
if (root !== null) {
entangleTransitions(root, current, lane);
}
return lane;
}
这个函数里:
- 首先取出了我们的 FiberRootNode 的根节点,结合 lane(优先级)信息创建了一个更新,关于这个优先级我们稍后会说
- 之后将 update 添加到根节点的更新链表中,您可以回去阅读第二章 Fiber 的结构,您可以发现在 Fiber 中有一个 updateQueue 字段( 更新列表 ),createUpdate 字段就是把更新写入这个字段,在这个更新里,我们放入了我们传入的 React Element
- 最后我们调用了 scheduleUpdateOnFiber ,它刚刚放入的 React Element 取出,构建出下一个 fiber 节点
scheduleUpdateOnFiber 的调用
下面我们来看看 scheduleUpdateOnFiber 做了什么,它在源码的这个位置:
https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberWorkLoop.js
省略一些优先级和调度相关的代码,最后我们可以简化整个流程为:
- 自底向上更新整个优先级并会返回更新后的整个 Fiber 树的根节点
- 标记 Root 的更新
- 之后
ensureRootIsScheduled
这个函数传入根节点注册一个调度任务
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
): FiberRoot | null {
// 检查是否有循环更新
checkForNestedUpdates();
// 自底向上更新整个优先级
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
return null;
}
// 标记 root 有更新,将 update 的 lane 插入到 root.pendingLanes 中
markRootUpdated(root, lane, eventTime);
// 省略进程调度相关的,暂时按下不表
if (
(executionContext & RenderContext) !== NoLanes &&
root === workInProgressRoot
) {
//.....省略
} else {
//.....省略进程调度相关的
// 注册调度任务, 由 Scheduler 调度, 进行 Fiber 构造
ensureRootIsScheduled(root, eventTime);
if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode &&
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
//.....同步相关的暂时按下不表
}
}
return root;
}
ensureRootIsScheduled 生成两类调度任务
那么接下来我们进入 ensureRootIsScheduled 这个函数,它和 scheduleUpdateOnFiber 在一个目录下,他主要做了两部分的事情,第一是判断我们是不是需要注册新的调度,然后注册一个新的调度来执行我们的 Fiber 生成逻辑,我们可以看到,根据各种情况的分支最后都调用了performSyncWorkOnRoot
或者 performConcurrentWorkOnRoot
函数。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
// 判断是否需要注册新的调度
markStarvedLanesAsExpired(root, currentTime);
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
if (nextLanes === NoLanes) {
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// 启动一个新的调度任务
const newCallbackPriority = getHighestPriorityLane(nextLanes);
const existingCallbackPriority = root.callbackPriority;
// .....省略 DEV 模式代码
if (existingCallbackNode != null) {
cancelCallback(existingCallbackNode);
}
let newCallbackNode;
//判断是不是同步任务
if (newCallbackPriority === SyncLane) {
// 判断是不是 legacy 模式 ( 老版本 )
if (root.tag === LegacyRoot) {
// ...省略进程调度
//同步任务调用 scheduleLegacySyncCallback 和 performSyncWorkOnRoot
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
//同步任务调用 scheduleSyncCallback 和 performSyncWorkOnRoot
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
// ...省略
newCallbackNode = null;
} else {
// ...省略进程调度
// 可中断的任务调用 scheduleCallback 和 performConcurrentWorkOnRoot
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
处理 SyncWork 和 ConcurrentWork
从这两个名字可以看出,我们的任务现在被分为了两类,一个是 SyncWork
,另一个是 ConcurrentWork
,他们分别对应同步任务和可中断任务:
- 同步任务就是我们需要等待这个任务完成的任务,类似 React 15.X 以及之前的模式,我们再它结束之前不能中断它
- 而可中断任务就是 React 16.X 之后的模式,我们可以在任务每个片执行完毕后中断它去执行优先级更高的任务,之后再继续执行它
这两个函数也有一条自己对应的调用链,这些函数的代码也都在同一个文件中,需要的可以自行阅读相关的内容:
performConcurrentWorkOnRoot
—— renderRootConcurrent
—— workLoopConcurrent
performSyncWorkOnRoot
—— renderRootSync
—— workLoopSync
在两个renderRoot
函数中,我们可以观察到,他们都需要调用 prepareFreshStack
这个函数,它的作用是:
我们之前提到过,React 中有 current
和 WorkInProgress
两棵树,他们通过 alternate
属性相互指引,但是在首次调用 render
方法的时候,我们的只初始化了 current
树,我们的 WorkInProgress
树还是空,但是在后续的操作中,我们需要同时用到这两棵树,所以我们需要通过当前的 current
树(只有根节点)来初始化一个 WorkInProgress
树,以下是这个函数的代码:
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
root.finishedWork = null;
root.finishedLanes = NoLanes;
workInProgressRoot = root;
//创建一个新的 rootWorkInProgress 树,逻辑和 current 树的创建方法类似
const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
return rootWorkInProgress;
}
在这个函数中,createWorkInProgress 利用传入的 current 节点创建出 WorkInProgress
的根节点;它判断 current.alternate 是否为空,若为空则创建出一个新节点;若不为空,则复制一份节点的属性,然后将传入的属性给到这个节点。之后把 workInProgress 指针指向到初始化出来的根节点,这个指针我们后面还会用到。
之后我们来到 workLoopConcurrent
和 workLoopSync
这两个函数中,可以看到他们的逻辑几乎一致,首先需要判断 workInProgress 是不是存在,也就是我们刚刚初始化出来的 workInProgress 树,唯一的区别是 workLoopConcurrent
还需要判断 shouldYield
的值,简单的来说 shouldYield
就是判定有没有中断我们当前任务的因素存在,这个我们后面会提到,现在你只需要知道这两种任务的执行是有区别的就行了:
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
performUnitOfWork
根据上面的代码,我们可以看到 performUnitOfWork
是两个函数都调用的公共的函数,也就是说两种任务最后都是调用了 performUnitOfWork
这个函数来处理,而它传入了我们刚刚初始化的 workInProgress
树, 现在我们来看看这个函数:
function performUnitOfWork(unitOfWork: Fiber): void {
//取出 current 树,传入的是 workInProgress 树的 root 节点,所以它的 alternate 指向的是 current 树的 root 节点
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
//调用 beginWork 创建 Fiber 节点,它是遍历整个 element 后得到的下一个节点,第一次调用则是我们传入的 element 的根节点
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
//如果没有下一个节点了(遍历结束了),那么结束整个流程,否则将 workInProgress 更新为刚刚新建的那个节点
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
在这个函数中, beginWork
会根据当前传入节点中的 element 结构,创建出新的 Fiber 节点,然后更新我们的 current 树和 workInProgress 树,如果它返回 null ,说明它没有孩子了,执行 completeUnitOfWork
处理函数,否则更新我们的 workInProgress,根据上文的逻辑,如果我们的 workInProgress 不是 null,那么我们将继续调用 performUnitOfWork
函数。至于 beginWork
函数内部做了什么,我们将放到下一篇中讲解。
completeUnitOfWork
当 element 中所有的子元素被遍历完毕,我们就将调用 completeUnitOfWork
函数进行处理,我们来看看这个函数,这里我们判断这个元素有没有兄弟节点,如果有兄弟节点,那么把 workInProgress
设为兄弟节点,按照上文的逻辑,此时 workInProgress
不是 null 了,整个 beginWork 的流程会继续执行。
如果没有兄弟节点了,那么把 completedWork
设置为当前节点的父节点,因为遍历的结束条件是 completedWork
不是空,也就是 completedWork
不是根节点,那么如果父节点有兄弟的话,函数就继续执行。直到返回的当前节点的父节点是根节点,说明遍历完成。
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return; // 该节点的父级节点
//省略....
//如果当前节点还有兄弟节点,获得兄弟
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
//继续执行我们的 beginWork
workInProgress = siblingFiber;
return;
}
//返回父节点
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
//遍历结束,标记遍历完成
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}
一个简单的例子
也就是说 performUnitOfWork
和 completeUnitOfWork
合作完成了一个深度优先搜索的逻辑,我们来看一个例子,比如下面的组件:
const FuncComponent = () => {
return (
<p>
<span>this is function component</span>
</p>
);
};
class ClassComponent extends React.Component {
render() {
return <p>this is class component</p>;
}
}
function App() {
return (
<div className="App">
<FuncComponent />
<ClassComponent />
<div>
<span>123</span>
</div>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
在这里例子中,我们的遍历顺序是:
- 首先是进入
performUnitOfWork
, App 组件,它自顶向下遍历第一个孩子,依次是 APP , div , FuncComponent , p , span , this is function component - 此时我们的元素的孩子是 null 了,进入
completeUnitOfWork
的函数,此时节点没有兄弟,所以返回上一级的节点,它也没有兄弟,继续向上,直到返回到 FuncComponent ,它有兄弟节点,那么把 workInProgress 设置成兄弟 ClassComponent ,继续遍历 - ClassComponent 有孩子节点,依次遍历 p,this is class component,然后进入
completeUnitOfWork
,当前节点依旧没有兄弟,依次返回到 ClassComponent,然后把当前节点置为其兄弟 div - 依次遍历 div ,span , 123,再次进入
completeUnitOfWork
,这次遍历依次返回父亲节点路上均没有兄弟,直到返回到根节点,遍历结束
注:这张图来自网上大神
总结
这一节里我们主要就讲解了 updateContainer 的函数调用链,它怎么样将我们传入的 React Element 变成 Fiber 树:
- 首先它调用了
createUpdate
创建了一个更新,将我们的 element 放到了更新中 - 之后调用
scheduleUpdateOnFiber
,根据自顶向上更新整个优先级,然后注册一个调度任务 - 然后调用
ensureRootIsScheduled
,根据任务的类型分别生成对应的任务,同步任务或者可中断任务 - 两类任务分别调用各自的三个函数,完成了生成
WorkInProgress
树的准备工作,然后进入workLoop
循环 - 在两个任务的
workLoop
循环中,都调用了performUnitOfWork
函数来操作 element performUnitOfWork
和completeUnitOfWork
合作完成了一个深度优先搜索的逻辑,遍历了整个 DOM 树,生成了 Fiber 树
现在我们已经理清楚了从 render 入口开始到生成整个 Fiber 的整体逻辑,它是以一个 DFS 的逻辑来遍历整个 DOM 然后生成了我们需要的 Fiber 链表树,对于整个转换的内容,我们还差两部分内容没有解决
- beginWork 函数做了什么操作
- 优先级和调度相关的内容我们之前都暂时忽略了,之后我们需要将这部分内容也加入我们的理解中
这两个问题将在之后的文章中一一解答。
参考资料
Wenzi 大神的教程 https://www.xiabingbao.com/post/react/jsx-to-fiber-riduz4.html