我们之前了解到:
- render 阶段开始的函数是 renderRootSync
- commit 阶段开始的函数是 commitRoot
注意:这里所说的render阶段不是render调用函数。而是指渲染阶段,在调用render函数之后的执行。
接下来会来介绍render阶段中都做了哪些工作。我们之前知道render阶段是实现可中断的递归(时间切片),从而提升性能。 那递归就分为递阶段、归阶段两部分:
- 递阶段执行的方法是 beginWork
- 归阶段执行的方法是 completeWork
接下来是对他们进行研究,看看到底干了些啥玩儿意(ying),哈哈哈哈,东北话。
首先我们的App.js文件中代码如下:
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
mount
大概流程
Fiber双缓存架构在mount时不存在current Fiber树,而是在update时存在一颗current Fiber树。
所以beginWork和completeWork在mount时和update时会有不同。
接下来看看初次进入beginWork的时候都做了什么: 在你的项目中看这个文件:/react_origin_code_debugger/react/build/node_modules/react-dom/cjs/react-dom.development.js 里面的beginWork代码如下:
function beginWork(current, workInProgress, renderLanes) {
// workInProgressRoot: FiberNode
// current: FiberNode
// 首次进入 beginWork 的节点,可以看到 current.tag === 3
// current.tag === 3 对应的是 HostRoot--表示当前对应的根节点
// 再次进入的时候 current === null
{
if (workInProgress._debugNeedsRemount && current !== null) {
return remountFiber(
current,
workInProgress,
createFiberFromTypeAndProps(workInProgress.type, workInProgress.key, workInProgress.pendingProps, workInProgress._debugOwner || null, workInProgress.mode, workInProgress.lanes));
}
}
if (current !== null) {
var oldProps = current.memoizedProps;
var newProps = workInProgress.pendingProps;
if (oldProps !== newProps || hasContextChanged() || (
workInProgress.type !== current.type )) {
didReceiveUpdate = true;
} else {
var hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
if (!hasScheduledUpdateOrContext &&
(workInProgress.flags & DidCapture) === NoFlags) {
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
didReceiveUpdate = true;
} else {
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
}
workInProgress.lanes = NoLanes;
// 先忽略这里
switch (workInProgress.tag) {...}
{
{
throw Error( "Unknown unit of work tag (" + workInProgress.tag + "). This error is likely caused by a bug in React. Please file an issue." );
}
}
}
首次进入 beginWork 的节点,可以看到 current.tag 的值为 3。current.tag: 3 对应的是 HostRoot--表示当前对应的根节点(FiberNode)
tag的表示含义可以在项目中这里看到:/react_origin_code_debugger/react/packages/react-reconciler/src/ReactWorkTags.js
如果没有本地可以去这里找一下:github.com/sunkuangdon… 也可以直接clone下来,本地运行起来。
当我们再次进入到 beginWork 的时候,current === null,下图可以看到
我们之前Fiber架构的双缓存架构中介绍过:对于首屏渲染,只有在当前应用的根节点存在 current Fiber,而其他节点只存在 workInProgress Fiber。
下图中,可以看到当前的 workInProgress Fiber 是 App:
首次进入的时候 workInProgress fiber 与 current Fiber 是一样的。如果不信,可以打debugger再看一眼,这里就不展示了。
可见之前的 Fiber双缓存架构 是正确的分析。哈哈哈哈,先夸一波~
第三次进入 beginWork 的时候,workInProgress Fiber 是 div:
第四次进入 beginWork 的时候,workInProgress Fiber 应该是header了,然后按照App.js的层级进行递进。
到了 img 没有子节点,而是有兄弟节点 p 和 a。这时候从 img 节点开始,跳出 beginWork 函数,执行 completeWork 函数。
completeWork 函数的 workInProgress Fiber 是 img,从而进行对img兄弟节点的查找。
img 有一个兄弟节点 p,然后会进入 p 节点的 beginWork
p节点有三个子节点,再次进入 beginWork 会看到子节点的文本节点:Edit
而文本节点 Edit 没有子节点,会进入 completeWork 函数,当前节点为 Edit 文本节点:
然后 会进入 code 节点的 beginWork 函数,再次进入你会发现此时不是beginWork了,而是completeWork函数。但是 code 明明有一个子节点:src/App.js。
这是因为React对于只有唯一一个文本子节点的节点,进行了优化处理。在这种情况下,文本节点将不会再生成自己的Fiber节点。
在接下来,code执行完 completeWork 之后,会查找生成code的兄弟节点,这里是:and save to reload. 文本节点。
在执行,肯定是and save to reload. 文本节点的 completeWork,执行完之后会进入父节点的 completeWork,也就是 p 节点的 completeWork。
当 p 节点的 completeWork 执行完之后,会进入 p 节点兄弟节点 a 的 beginWork:
a 节点的唯一子节点是 Learn React文本节点,所以这个文本节点不会存在自己的Fiber节点。所以接下来会进入 a节点的 completeWork,然后一点点的往上,进入 header节点的 completeWork...最后进入的是:FiberNode 节点,当前的根节点。
此时 Render 阶段就完成了,进入 commit 阶段。
beginWork 的具体工作
上面是大概的流程,接下来看看 beginWork 函数到底做了啥?
我们知道第一次进入 beginWork 的是当前应用的根Fiber节点,他的 tag === 3。我们以 div 这个Fiber 节点来进行分析。
首先 beginWork 会根据不同的 tag 进入不同的 case,上面的case被忽略了,这里单独拿出来进行分析:
switch (workInProgress.tag) {
case IndeterminateComponent:
{
return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
}
case LazyComponent:
{
var elementType = workInProgress.elementType;
return mountLazyComponent(current, workInProgress, elementType, renderLanes);
}
case FunctionComponent:
{
var Component = workInProgress.type;
var unresolvedProps = workInProgress.pendingProps;
var resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderLanes);
}
case ClassComponent:
{
var _Component = workInProgress.type;
var _unresolvedProps = workInProgress.pendingProps;
var _resolvedProps = workInProgress.elementType === _Component ? _unresolvedProps : resolveDefaultProps(_Component, _unresolvedProps);
return updateClassComponent(current, workInProgress, _Component, _resolvedProps, renderLanes);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent$1(current, workInProgress, renderLanes);
case HostText:
return updateHostText$1(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
case ForwardRef:
{
var type = workInProgress.type;
var _unresolvedProps2 = workInProgress.pendingProps;
var _resolvedProps2 = workInProgress.elementType === type ? _unresolvedProps2 : resolveDefaultProps(type, _unresolvedProps2);
return updateForwardRef(current, workInProgress, type, _resolvedProps2, renderLanes);
}
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
case Mode:
return updateMode(current, workInProgress, renderLanes);
case Profiler:
return updateProfiler(current, workInProgress, renderLanes);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
case MemoComponent:
{
var _type2 = workInProgress.type;
var _unresolvedProps3 = workInProgress.pendingProps;
var _resolvedProps3 = resolveDefaultProps(_type2, _unresolvedProps3);
{
if (workInProgress.type !== workInProgress.elementType) {
var outerPropTypes = _type2.propTypes;
if (outerPropTypes) {
checkPropTypes(outerPropTypes, _resolvedProps3,
'prop', getComponentNameFromType(_type2));
}
}
}
_resolvedProps3 = resolveDefaultProps(_type2.type, _resolvedProps3);
return updateMemoComponent(current, workInProgress, _type2, _resolvedProps3, renderLanes);
}
case SimpleMemoComponent:
{
return updateSimpleMemoComponent(current, workInProgress, workInProgress.type, workInProgress.pendingProps, renderLanes);
}
case IncompleteClassComponent:
{
var _Component2 = workInProgress.type;
var _unresolvedProps4 = workInProgress.pendingProps;
var _resolvedProps4 = workInProgress.elementType === _Component2 ? _unresolvedProps4 : resolveDefaultProps(_Component2, _unresolvedProps4);
return mountIncompleteClassComponent(current, workInProgress, _Component2, _resolvedProps4, renderLanes);
}
case SuspenseListComponent:
{
return updateSuspenseListComponent(current, workInProgress, renderLanes);
}
case ScopeComponent:
{
break;
}
case OffscreenComponent:
{
return updateOffscreenComponent(current, workInProgress, renderLanes);
}
case LegacyHiddenComponent:
{
return updateLegacyHiddenComponent(current, workInProgress, renderLanes);
}
case CacheComponent:
{
{
return updateCacheComponent(current, workInProgress, renderLanes);
}
}
}
div 是一个 HostComponent,所以他会进入:updateHostComponent$1 函数
updateHostComponent$1 函数首先会复制很多变量,其中var isDirectTextChild = shouldSetTextContent(type, nextProps)
是为了检测:当前Fiber节点是否只有唯一一个文本子节点,从而进行优化(将不会为文本子节点创建Fiber节点)。
接下来会进入 reconcileChildren 方法,在执行这个方法之前,当前 workInProgress.child === null
,所以 reconcileChildren 这个方法会为当前的Fiber节点创建他的子Fiber节点。
其实他的函数名已经告诉我们了,当前的执行阶段是render阶段,render阶段是运行在协调器(reconciler)中的....
进入 reconcileChildren ,下图中能够看到,通过判断 current 是否等于 null 来进入mountChildFibers 或者 reconcileChildFibers
这俩方法有什么区别呢?
我们进入项目中:/react_origin_code_debugger/react/packages/react-reconciler/src/ReactChildFiber.old.js文件里面。下图中能够看见,这两个函数其实都是 ChildReconciler 函数调用的返回值,只不过 ChildReconciler 接受的参数不同
ChildReconciler 参数的含义:是否追踪副作用。
- 我们以 deleteChild 函数为例,能够看到如果为false说明不追踪副作用,直接return。
- 如果追踪副作用,deletions 中会追加 childToDelete
render 阶段会为具体要执行的 DOM 操作打上标记,具体去执行这些 DOM 操作是 commit 阶段。 下图就是对 DOM 操作打上的部分标记
比如:
- Placement:Fiber节点对应的DOM节点需要插入在页面中。
- Deletion:Fiber节点对应的DOM节点需要删除。
为什要用二进制的方式进行标记?
如果有一个Fiber节点对应的DOM节点,首先需要插入到页面中,然后需要更新他的属性,那么他就需要同时存在Placement、Update两个标记。利用 |= 操作运算符可以让一个变量同时拥有两个操作。
如何将DOM插入到页面中,会在下一篇中讲解。
接下来看:reconcileChildren 函数中 reconcileChildFibers 方法。
reconcileChildFibers 方法中他会判断当前 newChild 的类型,根据类型不同作出不同处理。
总结
当某一个Fiber节点进入 beginWork 时,他最终的目的是创建当前Fiber节点的第一个子Fiber节点。
- 首先,会判断当前Fiber节点的类型,进入不同的 Update 的逻辑;
- 然后在 Update 的逻辑中,他会判断当前workInProgress Fiber 是否存在对应的 current Fiber,来决定是否标记 EffectTag。(reconcileChildren)。
- 接下来会进入 reconcile (reconcileChildFibers) 的逻辑,会判断当前Fiber 的 child 节点是什么类型,来执行不同的创建操作。最终会创建一个子Fiber节点 (FiberNode)
上面的括号中是执行的部分关键函数。注意每一次 beginWork 方法的执行,都只会创建一个Fiber节点,哪怕里面有Array类型。