2024年1月12日,星期五,我觉得在React源码方面,经历了很多次的挣扎、理解、痛苦后,我终于入门了。本次给大家带来的东西是fiber树的构建。拆解的会比较细,所以,请大家系好安全带,我要发车了。
本次讲解的React源码是17版本(当然,后续肯定会更新React18版本的讲解),请看如下代码:
react
import React from 'react';
import ReactDOM from 'react-dom';
class Component1 extends React.Component {
constructor(props) {
super(props)
}
render(){
return <div className = 'component-father-1'>
<div className = 'component-child-1'>
<div className = 'c1'>qw1</div>
<div className = 'c2'>qw2</div>
<div className = 'c3'>qw3</div>
</div>
</div>;
}
}
ReactDOM.render(
<Component1 />,
document.getElementById('app')
);
从上图我们可以看到,ReactDOM.render实际上是调用了LegacyRenderSubtreeIntoContainer
方法,而我们的<Component1 />
组件在传入到render方法里的时候,就已经被转换为了虚拟DOM。那它是如何被转为虚拟DOM的?一句话:babel碰到<Component />这种写法,会把它转译为React.createElement方法,这个方法会返回一个对象,这个对象就叫做虚拟DOM,可以用来描述组件信息
。
等等,我们好像跑题啦,接着看一下LegacyRenderSubtreeIntoContainer
方法吧:
jsx
ReactDOM.render( <Component1 />, document.getElementById('root') );
// 等价于下面的写法
legacyRenderSubtreeIntoContainer(
null,
{
$$typeof: Symbol(react.element),
props: {},
type: 名为Component1的构造函数
},
container,
false,
undefined
);
上图就是LegacyRenderSubtreeIntoContainer
方法的源码,从这里开始,我希望大家每次读完一段讲解,都要去回顾一下之前讲到的内容。
虽然代码有点多,但是我们会发现,上述代码可以精简如下:
jsx
function legacyRenderSubtreeIntoContainer(null, children, container, false, undefined) {
var root = container._reactRootContainer;
if (!root) {
// Initial mount
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, false);
fiberRoot = root._internalRoot;
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, null, undefined);
});
}
}
这个方法首先创建了一个对象,并将这个对象赋值给了 root、container._reactRootContainer。这个对象的样子如下:
仔细观察上图与代码,这个时候出现了一个隐藏的关系:
到目前为止,我相信很多小伙伴应该已经疑惑,你给我弄那么多意思相近的名词干什么。请注意,这些名词不是我定义,是代码里给到的信息而已,我只是先将它图形化了。
紧接着,我们就执行了updateContainer
方法。调用这个方法的具体信息如下:
jsx
updateContainer(
{
$$typeof: Symbol(react.element),
props: {},
type: 名为Component1的构造函数
},
FiberRoot,
null,
undefined
)
updateContainer方法里,先是计算fiber的优先级,然后再根据优先级创建更新对象,随后又将update存了起来,最后是调度当前的fiber节点。
jsx
function updateContainer(element, container, null, undefined) {
var current$1 = container.current;
// 其余代码在本次内容里不必关注
scheduleUpdateOnFiber(current$1, lane, eventTime);
}
我们现在来看看这个current$1
是个什么意思。current$1就是FiberRoot.current,也就是fiberNode,这个fiberNode是谁的描述对象呢?大家可以看一下当前fiberNode的信息,它其实就是id为app的dom节点对应的fiber节点。
我们接着来看一下scheduleUpdateOnFiber
方法,我们来精简一下代码,只关注跟本次内容相关的东西:
jsx
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
// root就是fiberRoot
var root = markUpdateLaneFromFiberToRoot(fiber, lane);
// 将整个fiberRoot传给下面的方法
performSyncWorkOnRoot(root);
}
注意,注意啊,markUpdateLaneFromFiberToRoot
返回的是fiberRoot
,还记得下面这张图吗:
接下来我们再看一下performSyncWorkOnRoot
方法。这个方法精简后的代码如下:
jsx
function performSyncWorkOnRoot(root){
// 忽略其余代码.........
renderRootSync(root, 1);
// 忽略其余代码.........
}
紧接着我们来看一下renderRootSync代码,我们重点关注下面的几行代码:
jsx
function renderRootSync(root, lanes) {
// 忽略其余代码.....
prepareFreshStack(root, lanes);
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// 忽略其余代码.....
}
function workLoopSync(){
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
从上面的代码来看,prepareFreshStack
方法的目的就是创建全局变量workInProgress
,好能开启工作循环,而在工作循环中,fiber树一点一点的被创建出来。
我们先来看看prepareFreshStack
是如何创建workInProgress的:
jsx
function prepareFreshStack(root, lanes) {
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);
}
上面的方法其实就是根据当前id为app的dom对应的fiber(下面都以rootFiber代替)
再创建一个fiber,这个新的fiber就是workInProgress,具体关系如下:
现在我们已经成功的创建了workInProgress,接下来就该进入工作循环了:
jsx
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
function performUnitOfWork(unitOfWork) {
var current = unitOfWork.alternate;
setCurrentFiber(unitOfWork);
var next;
next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
resetCurrentFiber();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 这块暂时不用管
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner$2.current = null;
}
React源码有一点好处,即使你没读过performUnitOfWork
方法,你也能猜出来他要干啥。从根元素向下,不断的通过 beginWork方法 查找自己的子元素,并将他们连接起来
。
接下来我们就看看,React是如何通过beginWork方法将 当前的rootFiber(workInProgress) 与 我们的<Component1 /> 组件关联起来的:
jsx
function beginWork(current, workInProgress, renderLanes){
// 省略其他代码...
switch (workInProgress.tag) {
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case ClassComponent:
return updateClassComponent(current, workInProgress, _Component2, _resolvedProps, renderLanes);
}
// 省略其他代码....
}
beginWork其实就是根据当前fiber类型不同,做不同的处理,但中心思想都是创建子元素的fiber对象并返回,下面列举了几个fiber类型。
HostRoot
:根元素对应的fiber(注意,它并不是fiberRoot,它是rootFiber,忘记的小伙伴回顾一下之前的图片)。
ClassComponent
: 类组件对应的fiber。
HostComponent
: dom节点对应的fiber。
FunctionComponent
: 函数式组件对应的fiber。
因为workInProgress
当前指向的是 rootFiber
,所以此时会调用 updateHostRoot
方法,这个方法主要干了以下几件事:
- 调用reconcileChildren方法创建子fiber对象。
- 并将子fiber对象赋值给 workInProgress.child方法。
- 返回第一步新创建的子fiber对象。
其中,reconcileChildren方法就是React Diff算法,这个算法的主要目的就是创建新的rootFiber。经过上面的步骤后,我们可以得到下面的状态:
拿到上面的状态后,意味着beginWork方法也结束了。此时回退到performUnitOfWork方法里,将next指针指向最新的fiber节点,我们又可以得到下一个状态:
得到这个状态后,意味着本轮的workloop结束了,开启下一轮workloop。
开启下一个workloop,class组件的fiber是如何拿到它的子元素呢?我们别忘了,当前Component1组件可是用class写的,所以beginWork会调用updateClassComponent
方法,而这个方法最终会通过instance.render()来拿到当前class组件里的子元素。我们此时会得到下一个状态:
再开启下一个workloop,对于一个dom节点来说,它如何构建下一个子fiber呢?因为dom原声标签对应的tag是HostComponent,所以调用beginWork时,其实是调用的updateHostComponent方法
。当我们给一个原生标签创建fiber时,我们是一定能够知道当前标签的children是谁(为什么?因为React在工作前,一定会经过babel处理,我们是可以拿到组件对应的虚拟dom的),然后把children属性放到pendingProps里,这样再构建子fiber时,直接从pendingProps里获取子元素就可以了。
拿到子元素后,构建fiber的流程跟上面一样,我们又得到了一个状态:
再开启一轮工作循环,当前workInProgress对应的子节点有3个,分别是c1、c2、c3。因为子元素有多个,所以再进行diff算法的时候,会调用reconcileChildrenArray
方法,然后每个子fiber之间通过sibling连接,形成下面的最终态:
总结
下面来用一句话总结一下fiber树的构建:
如果React版本是16、17,那么React会首先为app建立rootFiber,随后开启工作循环,工作循环的目的就是根据当前fiber,去创建子fiber,并将子fiber通过return、child属性跟当前fiber关联起来,最终形成一颗fiber树
。
最后
这篇文章终于写完了,写React源码方面的文章真是太累了,因为React关联太密切了,你又很想讲细,但又怕文章字数太多,所以很容易导致烂尾~~