我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 我们在文章 React18 源码解析之 fiber 等数据结构 中讲解了 jsx, element 和 fiber 的基本结构。这里我们主要讲下如何将 jsx 转为 fiber 节点组成的 fiber 树。 我们为便于理解整个转换的过程,会做一些流程上的精简: 在开始讲解前,我们先定义下要渲染的 React 组件,方便我们后续的理解: 我们编写这个结构主要是为了看下,嵌套的、并列的标签,流程是如何流转的,中间经历了多少个复杂的过程,才将 element 转为 fiber 节点。 我是强烈建议使用debug-react项目来进行调试的,因为里面的分支和各种变量很多,若只是单纯地硬看,很容易懵,不知道这些变量具体是什么值!那么运行起一个 React 项目后,可以在某些关键节点、变量,打断点输出,就方便很多。 render()传入的 element 和后续的 element 的操作是不一样的。其他的 element 都可以通过执行函数组件或者类组件的实例来得到,而初始的 element 是直接提供的。 我们直接输出下 可以看到,element 结构并不是把 React 所有的 jsx 都组织起来,形成巨大的嵌套结构,他只是当前某个节点里的 jsx 结构。若当前是函数组件、类组件等,内部的 jsx 可以通过执行属性 type 对应的函数或类的实例来得到,然后继续递归下去,最终把所有的 jsx 都全部解析出来。 入口函数 render() 传入的参数,就是上面 我们去掉 dev 代码和任务优先级的调度,看下 updateContainer() 主要的流程: 我们再梳理下函数 updateContainer()的流程: 接下来我们看下 scheduleUpdateOnFiber 函数。 markUpdateLaneFromFiberToRoot(fiber, lane)方法会将当前 fiber 节点往上知道 FiberRootNode 所有节点赋值 lane 这个优先级,同时返回整个应用的根节点 FiberRootNode。 我们忽略中间任务调度的步骤,直接进入到 ensureRootIsScheduled(root) 函数中,这里的 root 参数就是整个应用的根节点 FiberRootNode。这个函数里也是一堆的任务调度,我们快进到 performConcurrentWorkOnRoot.bind(null, root),这里面又快进到了 renderRootSync(root)。 这里有一个很重要的准备操作,这里的 root 是整个应用的根节点,即 FiberRootNode,会传入到函数 prepareFreshStack() 中,主要是为了接下来的递归,初始化一些数据和属性: 我们看下函数 prepareFreshStack() 的实现: 总结下 prepareFreshStack() 的作用: 准备好之后,我们再回到函数 renderRootSync(),就可以顺着 workInProgress 指针往下进行了,这里我们就进入到了函数 workLoopSync() 中。 该函数很简单,就是一个 while 循环,每次循环时,都会执行函数 performUnitOfWork() ,然后操作 workInProgress 指向的那个 fiber 节点,直到 workInProgress 为 null。 我们刚才在上面的 prepareFreshStack() 中,workInProgress 指针指向到了「更新树」的根节点 rootWorkInProgress(即跟 current 树根节点长得一样的那个节点),这个 fiber 节点里的 updateQueue.shared.pending 中的一个 update 里,存放着 element 结构。 处理 workInProgress 指向的那个 fiber 节点。 将当前 current 节点和更新树的节点,都传给 beginWork(current, unitOfWork) 函数。简单来说,beginWork 会根据当前 fiber 节点中的 element 结构,创建出新的 fiber 节点,workInProgress 再指向到这个新 fiber 节点继续操作,直到所有的数据都操作完。具体的操作流程,我们单独开一篇文章进行讲解。 这里我们只需要知道,诸如函数组件、类组件也是 fiber 节点,也是整棵 fiber 树的一部分。其内部的 jsx(element)再继续转为 fiber 节点。 若 beginWork()返回的 next 是 null,说明当前节点 workInProgress 已经是最内层的节点了,就会进入到函数 completeUnitOfWork() 中。可以看到执行的流程是 以我们上面的写的 React 组件为例,workInProgress 指向是的树的根节点,这个根节点没有具体的 jsx 结构。 我们接下来再看下函数 completeUnitOfWork() 是如何流转 workInProgress 指针的。 当前节点和当前所有的子节点都执行完了,就会调用该方法。现在我们只关心整个流程的流转问题。 通过这张图,我们可以更加直观地理解整个流转流程: 图中的数字,就是 workInProgress 指针行进的顺序。 所有的节点都遍历执行完了,workLoopSync()中的 while()循环也就停止了,workInProgress 也指向到了 null,然后就可以进入到 DOM 渲染的 commit 阶段了。 到这里,我们把 element 转为 fiber 节点的大致流程过了一遍。主要了解到以下几个知识点: 接下来的文章,我们会再详细介绍下函数 beginWork(),了解不同的 element 结构如何转成 fiber 节点的。
render()
方法是我们整个应用的入口,我们就从这里开始。我们在之前的文章React18 源码解析之 render()入口方法中,只是讲解了 render()方法的挂载方式。这里我们会深入了解到从 jsx 转为 fiber 的整个过程。1. 起始 #
const FuncComponent = () => {
return (
<p>
<span>this is function componentspan>
p>
);
};
class ClassComponent extends React.Component {
render() {
return <p>this is class componentp>;
}
}
function App() {
return (
<div className="App">
<FuncComponent />
<ClassComponent />
<div>
<span>123span>
div>
div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
2. 初始的 element 如何处理? #
:console.log(<App />);
的 element 结构。内部通过属性_internalRoot
得到整个应用的根节点 root,然后又调用了 updateContainer():// children就是我们传入的
/**
* 将element结构转为fiber树
* @param {ReactNodeList} element 虚拟DOM树
* @param {OpaqueRoot} container FiberRootNode 节点
* @param {?React$Component
* @param {?Function} callback render()里的callback,不过从React18开始就没了,传入的是null
* @returns {Lane}
*/
export function updateContainer(element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component
callback: ?Function,
* current:
* const uninitializedFiber = createHostRootFiber(tag, isStrictMode, concurrentUpdatesByDefaultOverride,);
*/
// FiberRootNode.current 现在指向到当前的fiber树,
// 若是初次执行时,current树只有hostFiber节点,没有其他的
const current = container.current;
const eventTime = requestEventTime();
const lane = requestUpdateLane(current);
// 结合 lane(优先级)信息,创建 update 对象,一个 update 对象意味着一个更新
/**
* const update: Update<*> = {
* eventTime,
* lane,
* tag: UpdateState,
* payload: null,
* callback: null,
* next: null,
* };
* @type {Update<*>}
*/
const update = createUpdate(eventTime, lane);
update.payload = { element };
// 处理 callback,这个 callback 其实就是我们调用 ReactDOM.render 时传入的 callback
// 不过从React18开始,render不再传入callback了,即这里的if就不会再执行了
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
/**
* 将update添加到current的更新链表中
* 执行后,得到的是 current.updateQueue.shared.pending = sharedQueue
* sharedQueue是React中经典的循环链表,
* 将下面的update节点插入这个shareQueue的循环链表中,pending指针指向到最后插入的那个节点上
*/
enqueueUpdate(current, update, lane);
/**
* 这里调用的链路很深,做了很多事情,如:
* 流程图: https://docs.qq.com/flowchart/DS0pVdnB0bmlVRkly?u=7314a95fb28d4269b44c0026faa673b7
* scheduleUpdateOnFiber() -> ensureRootIsScheduled(root) -> performSyncWorkOnRoot(root)
* -> renderRootSync(root) -> workLoopSync()
*/
/**
* 这里传入的current是HostRootFiber的fiber节点了,虽然他的下面没有其他fiber子节点,
* 但它的updateQueue上有element结构,可以用来构建fiber节点
* 即 current.updateQueue.shared.pending = sharedQueue,element结构在sharedQueue其中的一个update节点,
* 其实这里只有一个update节点
*/
const root = scheduleUpdateOnFiber(current, lane, eventTime);
if (root !== null) {
entangleTransitions(root, current, lane);
}
return lane;
}
3. scheduleUpdateOnFiber #
4. renderRootSync #
/**
* 整个应用目前只有 FiberRootNode和current两个节点,current树只有一个根节点,就是current自己;
* 另一棵树还没有创建,结构是这样: https://mat1.gtimg.com/qqcdn/tupload/1659715740891.png
* prepareFreshStack() 函数的作用,就是通过current树的根节点创建出另一棵树的根节点,
* 并将这两棵树通过 alternate 属性,实现互相的指引
* workInProgressRoot: 是将要构建的树的根节点,初始时为null,经过下面 prepareFreshStack() 后,
* root.current给到workInProgressRoot,
* 即使第二次调用了,这里的if逻辑也是不会走的
* workInProgress初始指向到workInProgressRoot,随着构建的深入,workInProgress一步步往下走
*/
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
/**
* 将整个应用的根节点和将要更新的fiber树的根节点赋值到全局变量中
* root是当前整个应用的根节点
*/
prepareFreshStack(root, lanes);
}
/**
* 准备新堆栈,返回「更新树」的根节点
* @param root
* @param lanes
* @returns {Fiber}
*/
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
root.finishedWork = null;
root.finishedLanes = NoLanes;
workInProgressRoot = root; // 整个React应用的根节点,即 FiberRootNode
/**
* prepareFreshStack()个人认为是只在初始化时执行一次,root是整个应用的根节点,而root.current就是默认展示的那棵树,
* 在初始化时,current 树其实也没内容,只有这棵树的一个根节点;
* 然后利用current的根节点通过 createWorkInProgress()方法 创建另一棵树的根节点rootWorkInProgress
* createWorkInProgress()方法内则判断了 current.alternate 是否为空,来决定是否可以复用这个节点,
* 在render()第一次调用时,root.current.alternate 肯定为空,这里面则会调用createFiber进行创建
*/
const rootWorkInProgress = createWorkInProgress(root.current, null);
// 初始执行时,workInProgress指向到更新树的根节点,
// 在mount阶段,workInProgress是新创建出来的,与current树的根节点workInProgressRoot,肯定是不相等的
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
return rootWorkInProgress;
}
5. workLoopSync #
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
// 已经超时了,所以即使需要让出时,也不再做检查,直到把workInProgress执行完
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
6. performUnitOfWork #
function performUnitOfWork(unitOfWork: Fiber): void {
/**
* 初始mount节点时,unitOfWork 是上面workLoopConcurrent()中传入的 workInProgress,
* unitOfWork.alternate 指向的是 current
*/
const current = unitOfWork.alternate;
let next;
/**
* current为当前树的那个fiber节点
* unitOfWork为 更新树 的那个fiber节点
* 在初始mount节点,current和unitOfWork都是fiberRoot节点
* 在第一次调用beginWork()时,element结构通过其一系列的流程,创建出了第一个fiber节点,即
* next就是第一个fiber节点,然后next给到workInProgress,接着下一个循环
*/
next = beginWork(current, unitOfWork, subtreeRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// unitOfWork已经是最内层的节点了,没有子节点了
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
深度优先
,即若当前 fiber 还能构造出子节点,即一直向下构造。直到没有子节点后,才会流转到兄弟节点和父级节点。const FuncComponent = () => {
return (
<p>
<span>this is function componentspan>
p>
);
};
class ClassComponent extends React.Component {
render() {
return <p>this is class componentp>;
}
}
function App() {
return (
<div className="App">
<FuncComponent />
<ClassComponent />
<div>
<span>123span>
div>
div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
对应的 fiber 节点,然后给到变量 next;workInProgress 再移动这个 fiber 节点上;
子元素的 fiber 节点,即 div 标签对应的 fiber 节点;
,
和 div 标签,beginWork()会节点的不同类型,创建出不同的 fiber 节点,然后形成链表,再回到这个链表的第 1 个节点;即 workInProgress 指向了
生成的 fiber 节点;
里的 jsx 对应的 fiber 节点,即 p 标签(beginWork()只会一层一层的构建),workInProgress 再指向到 p 标签对应的 fiber 节点,继续构建 span 和文本对应的 fiber 节点;
的兄弟节点
;
里的 jsx 对应的 fiber 节点,即 p 标签;7. completeUnitOfWork #
/**
* 当前 unitOfWork 已没有子节点了
* 1. 若还有兄弟节点,将 workInProgress 指向到其兄弟节点,继续beginWork()的执行;
* 2. 若所有的兄弟节点都处理完了(或者没有兄弟节点),就指向到其父级fiber节点;回到1;
* 3. 直到整个应用根节点的父级(根应用没有父级节点,所以为null),才结束;
*/
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return; // 每个节点都有一个return属性,指向到其父级节点
// 若有兄弟节点,则继续执行兄弟节点
const siblingFiber = completedWork.sibling; // 该节点下一个兄弟节点
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
}
// 当前节点和兄弟节点全部遍历完毕,则返回到其父节点
// Otherwise, return to the parent
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null); // while的作用就是若该节点没有兄弟节点,能够一直往上找父级节点,
}
8. 总结 #
深度优先
的原则,优先执行其第 1 个节点,然后再执行兄弟节点,再回到父级节点;
版权属于:
加速器之家
作品采用:
《
署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
》许可协议授权
评论