# Render 阶段
# 流程概述
当组件更新,本质上是从 fiberRoot 开始深度调和 fiber 树。render 阶段的核心就是如何创建 Fiber Node 以及 构建 Fiber Tree。render 阶段开始于 performSyncWorkOnRoot
或 performConcurrentWorkOnRoot
方法的调用。这取决于本次更新是同步更新还是异步更新。这两个方法中会调用如下两个方法:
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
可以看到,他们唯一的区别是是否调用shouldYield
。如果当前浏览器帧没有剩余时间,shouldYield
会中止循环,直到浏览器有空闲时间后再继续遍历。
workInProgress
代表当前已创建的workInProgress fiber
。performUnitOfWork
方法会创建下一个Fiber节点
并赋值给workInProgress
,并将workInProgress
与已创建的Fiber节点
连接起来构成Fiber树
。
Fiber Reconciler
是从Stack Reconciler
重构而来,通过遍历的方式实现可中断的递归,所以performUnitOfWork
的工作可以分为两部分:“递”和“归”。
# “递”阶段
首先从rootFiber
开始向下深度优先遍历。为遍历到的每个Fiber节点
调用 beginWork方法。该方法会根据传入的Fiber节点
创建子Fiber节点
,并将这两个Fiber节点
连接起来。
当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。
# “归”阶段
在“归”阶段会调用 completeWork 方法 处理Fiber节点
。当某个Fiber节点
执行完completeWork
,如果其存在兄弟Fiber节点
(即fiber.sibling !== null
),会进入其兄弟Fiber
的“递”阶段。如果不存在兄弟Fiber
,会进入父级Fiber
的“归”阶段。
“递”和“归”阶段会交错执行直到“归”到rootFiber
。至此,render阶段
的工作就结束了。
- 组件 A 触发
setState
或者useState
更新视图,既然fiber
是从 root 开始更新,那么如何找到对应的 A 并 rerender 的呢? - 组件类型 fiber 进行
beginWork
就一定会进行render
吗?
# 最小更新单元 State
虽然在 React V18 引入订阅外部数据源的 useMutableSource
。但在当前版本的 React 中,视图的更新基本都来源于内部 state 的改变。如果有一个组件 A ,如果想要它更新,那么场景有如下情况:
- 组件本身改变
state
。函数useState
|useReducer
,类组件setState
|forceUpdate
。 props
改变,由组件更新带来的子组件的更新。context
更新,并且该组件消费了当前context
。
无论是上面哪种方式,本质上都是 state 的变化。
- props 改变来源于父级组件的 state 变化。
- context 变化来源于
Provider
中 value 变化,而 value 一般情况下也是 state 或者是 state 衍生产物。
state
改变是在组件对应的 fiber 单位上的,之前的 fiber 章节讲到了在 React 的世界里会存在多种多样的 fiber 类型, 而开发者平时使用的组件 function Component
或者 Class Component
也是两种不同的 fiber 类型。而且 React 底层对它们的处理逻辑也不相同。
- 比如更新类组件用的是
updateClassComponent
,它做的事情是初始化时候实例化类组件,更新的话那么直接调用 render 得到新的children
; - 更新函数组件用的是
updateFunctionComponent
,里面调用renderWithHooks
执行函数组件并依次调用hooks
。
那么在整个 React
系统中,能够更新 state 的基本都在组件层面上,换句话说只有组件才能出发更新,比如 div
元素 hostComponent 类型的 fiber,它是无法独立的自我更新的,只能依赖于父类的组件更新 state ,但是在调和阶段,它也会作为一个任务单元进入到 workLoop 中 ;综上所述,可以这么理解
- fiber是调和过程中的最小单元,每一个需要调和的 fiber 都会进入 workLoop 中。
- 而组件是最小的更新单元,React 的更新源于数据层 state 的变化。
# 调度任务的初始化工作
以组件类型的 fiber 调和流程来理解组件更新流程。类组件在 render 阶段的一个重要作用就是产生新的 children ,也就是我们常说的 rerender。只有产生新的 children ,接下来才能深度遍历 children ,改变视图。每一个需要调和的 fiber 都要经历一个过程叫做 beginWork,在 beginWork 流程中将执行上述各种 fiber 的更新函数。
那么对于组件类型 fiber 说,进入到 workLoop 中,那么一定会 rerender
吗? 答案是否定的,解析来看几种情况。
主要看一下如下 demo :
/* 子组件2 */
function Child2(){
return <div>子组件 2</div>
}
/* 子组件1 */
function Child1(){
const [ num , setNumber ] = React.useState(0)
return <div>
子组件 {num}
<button onClick={() => setNumber(num+1)} >按钮1</button>
</div>
}
/* 父组件 */
export default function Index(){
const [ num , setNumber ] = React.useState(0)
return <div>
<p>父组件 {num} </p>
<Child1 />
<Child2 />
<button onClick={()=> setNumber(num+1)} >按钮2</button>
</div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
场景一:如上 demo 中,当点击 Child1
的 按钮1 的时候,Child1 会渲染,那么 Child1 自然会进入到 beginWork
流程中,那么疑问来了:
- 问题一:父组件
Index
没有更新,会 rerender 吗?那么有会进入beginWork
流程吗 ? - 问题二:
Child2
会进入beginWork
流程吗 ? - 问题三:如果
Index
会beginWork
,那么 React 从 Root fiber 开始调和的时候,是如何找到更新的事发点 Index 的呢?
场景二:在如上 demo 中,当点击 Index 中的 按钮2 的时候:
- 问题四:
Index
因为本身的state
改变会更新,那么Child1
和Child2
为什么会跟着更新。
接下来我们开始以一次更新开始,分析调和过程中 beginWork 流程。
在正式流程分析之前,先来看一下 v17 引出的新的概念,在 v16 版本,任务的优先级用 expirationTime 表示,在 v17 版本被 lane 取缔。
- lane : 更新优先级。(在一次更新任务中,将赋予给更新的 fiber 的一个更新优先级 lane。)
- childLanes:
children
中更新优先级。(如果当前 fiber 的 child 中有高优先级任务,那么当前 fiber 的 childLanes 等于当前优先级)。
# scheduleUpdateOnFiber
React 更新任务的起点 - scheduleUpdateOnFiber
。
类组件 setState 更新
// react-reconciler/src/ReactFiberClassComponent.new.js -> classComponentUpdater
enqueueSetState(inst, payload, callback){
const fiber = getInstance(inst);
const lane = requestUpdateLane(fiber);
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
2
3
4
5
6
函数组件 useState 更新
// react-reconciler/src/ReactFiberHooks.new.js -> dispatchReducerAction
function dispatchReducerAction(fiber,queue,action){
const lane = requestUpdateLane(fiber);
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
2
3
4
5
如上代码都是精简后,保留的最核心的流程。可以明确看到,无论是组件更新的本质就是:
- 创建一个任务优先级 lane。
- 然后进行 scheduleUpdateOnFiber。 那么这个 scheduleUpdateOnFiber 应该就是整个 React 更新任务的开始。
# scheduleUpdateOnFiber 开始更新 fiber
// react-reconciler/src/ReactFiberWorkLoop.new.js -> scheduleUpdateOnFiber
function scheduleUpdateOnFiber(fiber,lane){
/* 递归向上标记更新优先级 */
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if(root === null) return null
/* 如果当前 root 确定更新,那么会执行 ensureRootIsScheduled */
ensureRootIsScheduled(root, eventTime);
}
2
3
4
5
6
7
8
scheduleUpdateOnFiber 主要做了两件事:
- 第一个就是通过当前的更新优先级 lane ,把当前 fiber 到 rootFiber 的父级链表上的所有优先级都给更新了。
- 如果当前 fiber 确定更新,那么会调用
ensureRootIsScheduled
# 优先级标记
那么 markUpdateLaneFromFiberToRoot 如何标记的优先级?
// react-reconciler/src/ReactFiberWorkLoop.new.js -> markUpdateLaneFromFiberToRoot
/**
* @param {*} sourceFiber 发生 state 变化的fiber ,比如组件 A 触发了 useState ,那么组件 A 对应的 fiber 就是 sourceFiber
* @param {*} lane 产生的更新优先级
*/
function markUpdateLaneFromFiberToRoot(sourceFiber,lane){
/* 更新当前 fiber 上 */
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
/* 更新缓存树上的 lanes */
let alternate = sourceFiber.alternate;
if (alternate !== null) alternate.lanes = mergeLanes(alternate.lanes, lane);
/* 当前更新的 fiber */
let node = sourceFiber;
/* 找到返回父级 */
let parent = sourceFiber.return;
while(parent !== null){
/* TODO: 更新 childLanes 字段 */
parent.childLanes = mergeLanes(parent.childLanes, lane);
if (alternate !== null) { alternate.childLanes = mergeLanes(alternate.childLanes, lane); }
/* 递归遍历更新 */
node = parent;
parent = parent.return;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
markUpdateLaneFromFiberToRoot 做的事很重要:
- 首先会更新当前 fiber 上的更新优先级。fiber 架构采用双缓冲树,所有还要更新当前 fiber 的缓冲树
alternate
上的优先级。 - 然后会递归向上把父级连上的 childLanes 都更新,更新成当前的任务优先级。
为什么向上递归更新父级的 childLanes ?
- 所有的 fiber 是通过一颗 fiber 树关联到一起的,如果组件 A 发生一次更新,React 是从 root 开始深度遍历更新 fiber 树。
- 那么更新过程中需要深度遍历整个 fiber 树吗?,当然也不是,那么只有一个组件更新,所有的 fiber 节点都调和无疑是性能上的浪费。
- 既然要从头更新,又不想调和整个 fiber 树,那么如何找到更新的组件 A 呢?这个时候
childLanes
就派上用场了,如果 A 发生了更新,那么先向上递归更新父级链的childLanes
,接下来从 Root Fiber 向下调和的时候,发现 childLanes 等于当前更新优先级,那么说明它的 child 链上有新的更新任务,则会继续向下调和,反之退出调和流程。
Root Fiber 是通过 childLanes 逐渐向下调和找到需要更新的组件的。
整个 fiber 树调和流程。
- 第一阶段是发生更新,那么产生一个更新优先级
lane
。 - 第二阶段向上标记 childLanes 过程。
- 第三阶段是向下调和过程,有的同学会问,为什么 A 会被调和,原因是 A 和 B 是同级,如果父级元素调和,并且向下调和,那么父级的第一级子链上的 fiber 都会进入调和流程。从 fiber 关系上看,Root 先调和的是 child 指针上的 A ,然后 A 会退出向下调和,接下来才是 sibling B,接下来 B 会向下调和,通过 childLanes 找到当事人 F,然后 F 会触发 render 更新。这也就解决问题2,Child2 的调和问题。
通过上述我们知道了如何找到 F 并执行 render 的,那么还有一个问题,就是 B,E 会向下调和,如果它们是组件,那么会 render 么,答案是否定的,要记住的是调和过程并非 render 过程,调和过程有可能会触发 render 函数,也有可能只是继续向下调和,而本身不会执行 render 。
既然知道了如何去更新 childLanes ,以及更新 childLanes 的意义,我们接着向下分析流程。在 scheduleUpdateOnFiber 中,最后会调用 ensureRootIsScheduled
,那么它的作用又是什么呢?
ensureRootIsScheduled 的作用就是根据任务的类型,发起异步调度任务,在调度章节已经讲了调度流程。接下来会走调度的流程。
- 对于
legacy sync
模式最后的更新任务是performSyncWorkOnRoot
。 - 对于
Concurrent
模式最后的更新任务是performConcurrentWorkOnRoot
。
# 从 workLoop 到 beginWork
这里主要以 legacy 模式为主,performSyncWorkOnRoot :
// react-reconciler/src/ReactFiberWorkLoop.new.js -> performSyncWorkOnRoot
function performSyncWorkOnRoot(root) {
/* render 阶段 */
let exitStatus = renderRootSync(root, lanes);
/* commit 阶段 */
commitRoot(root);
/* 如果有其他的等待中的任务,那么继续更新 */
ensureRootIsScheduled(root, now());
}
2
3
4
5
6
7
8
9
调和的两大阶段 render
和 commit
都在这个函数中执行。
renderRootSync
代表 render 阶段。commitRoot
代表 commit 阶段。- 当 render 和 commit 阶段执行之后,如果有其他的等待中的任务,那么继续执行调度任务。
到此为止,一次更新调度任务的初始化工作完成。开始正式进入调和阶段。对前戏阶段做一下总结,流程图如下:
# workLoop
renderRootSync 做了什么?
// react-reconciler/src/ReactFiberWorkLoop.new.js -> renderRootSync
function renderRootSync(root,lanes){
workLoopSync();
/* workLoop完毕后,证明所有节点都遍历完毕,那么重置状态,进入 commit 阶段 */
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
}
2
3
4
5
6
7
renderRootSync 核心功能:
- 执行
workLoopSync
。 workLoop
完毕后,证明所有节点都遍历完毕,那么重置状态,进入commit
阶段。
workLoopSync
在整个 render 流程中充当的角色非常重要,可以把 workLoopSync
当作一个循环运作的加工器,每一个需要调和的 fiber 可以当作一个零件,每一个零件都需要进入加工器,如果没有待加工的零件,那么加工器才停止运转。下面就是加工器的具体实现。
// react-reconciler/src/ReactFiberWorkLoop.new.js -> workLoopSync
function workLoopSync() {
/* 循环执行 performUnitOfWork ,一直到 workInProgress 为空 */
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
2
3
4
5
6
7
- 如上只要
workInProgress
不为null
(还有需要调和的 fiber),那么 workLoopSync 会循环调用performUnitOfWork
。
当 Concurrent 模式下会通过 shouldYield
,来判断有没有过期的任务,有过期任务,会中断 workLoop ,那么也就是说明了render阶段是可以被打断的。
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
2
3
# performUnitOfWork
回到 workLoopSync
流程上来,fiber 树是深度优先遍历得到的,在遍历完父节点,那么接下来就会遍历子节点。在这其中,每一个调和的 fiber 都将作为 workInProgress
进行调和更新。无论什么模式,workLoop 的执行单元都是 fiber 。而且更新单元的函数叫做 performUnitOfWork
。
// react-reconciler/src/ReactFiberWorkLoop.new.js -> performUnitOfWork
function performUnitOfWork(unitOfWork){
const current = unitOfWork.alternate;
let next = beginWork(current, unitOfWork, subtreeRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
2
3
4
5
6
7
8
9
10
11
# beginWork
# beginWork 发生场景
假设有一个组件 fiber 链:root Fiber --child--> A组件 --child--> B组件 --child--> C组件
。
以组件B 为参考,来看一下 React 如何调和的,那么一次更新就有可能有三种场景:
- 场景一:更新 A 组件,那么 A 触发更新,那么如果 B,C 没有做渲染控制处理(比如 memo PureComponent),那么更新会波动到 B , C,那么 A,B,C 都会 rerender。
- 场景二:当更新 B 组件,那么组件 A fiber 会被标记,然后 A 会调和,但是不会 rerender;组件 B 是当事人,既会进入调和,也会 rerender;组件 C 受到父组件 B 的影响,会 rerender。
- 场景三:当更新 C 件,那么 A,B 会进入调和流程,但是不会 rerender,C 是当事人,会调和并 rerender。
如上的场景本质上都在 beginWork 中进行的,那么 beginWork 是如何处理这些逻辑的:
# beginWork 的两个阶段
// react-reconciler/src/ReactFiberBeginWork.new.js -> beginWork
/**
* @param {*} current current 树 fiber
* @param {*} workInProgress workInProgress 树 fiber
* @param {*} renderLanes 当前的 render 优先级
* @returns
*/
function beginWork(current,workInProgress,renderLanes){
/* -------------------第一部分-------------------- */
// update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
// current!== null 来判断当前 fiber 是否创建过,如果第一次 mounted , 那么 current 为 null
// 第一阶段主要针对更新的情况。如果初始化,那么直接跳过第一阶段,到第二阶段。
if(current !== null){
/* 更新流程 */
/* current 树上上一次渲染后的 props */
const oldProps = current.memoizedProps;
/* workInProgress 树上这一次更新的 props */
const newProps = workInProgress.pendingProps;
if(oldProps !== newProps || hasLegacyContextChanged()){
didReceiveUpdate = true; // 当前更新是否来源于父级的更新 为 true 则来自父组件更新
}else{
/* props 和 context 没有发生变化,检查是否更新来自自身或者 context 改变 */
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current,renderLanes)
if(!hasScheduledUpdateOrContext){
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(current,workInProgress,renderLanes)
}
/* 这里省略了一些判断逻辑 */
didReceiveUpdate = false;
}
}else{
didReceiveUpdate = false
}
/* -------------------第二部分-------------------- */
/* TODO: 走到这里流程会被调和 | 更新,比如函数执行会执行,类组件会执行 render 。 */
// mount时:根据tag不同,创建不同的子Fiber节点
switch(workInProgress.tag){
/* 函数组件的情况 */
case FunctionComponent: {
return updateFunctionComponent( current, workInProgress, Component, resolvedProps, renderLanes )
}
/* 类组件的情况 */
case ClassComponent:{
return updateClassComponent(current,workInProgress,Component,resolvedProps,renderLanes)
}
/* 元素类型 fiber <div>, <span> */
case HostComponent:{
return updateHostComponent(current, workInProgress, renderLanes)
}
/* 其他 fiber 情况 */
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
注意
除 rootFiber 以外, 组件 mount 时,由于是首次渲染,是不存在当前组件对应的 Fiber 节点在上一次更新时的 Fiber 节点,即 mount 时 current === null
。组件 update时,由于之前已经 mount 过,所以 current !== null
。所以我们可以通过current === null ?
来区分组件是处于 mount 还是 update。
didReceiveUpdate
didReceiveUpdate :这个变量主要证明当前更新是否来源于父级的更新,那么自身并没有更新。比如更新 B 组件,那么 C 组件也会跟着更新,这个情况下 didReceiveUpdate = true。
基于此原因,beginWork
的工作可以分为两部分:
update
时:如果current
存在,在满足一定条件时可以复用current
节点,这样就能克隆current.child
作为workInProgress.child
,而不需要新建workInProgress.child
。mount
时:除fiberRootNode
以外,current === null
。会根据fiber.tag
不同,创建不同类型的子Fiber节点
# 第一阶段(update)
首先通过 current!== null
来判断当前 fiber 是否创建过,如果第一次 mounted , 那么 current 为 null,而第一阶段主要针对更新的情况。如果初始化,那么直接跳过第一阶段,**到第二阶段。**如果是更新流程。那么判断 oldProps === newProps(源码中还判断了老版本 context 是否变化),那么两者相等。一般会有以下几种情况:
情况一:还是回到上面场景上来,如果 C 组件更新,那么 B 组件被标记 ChildLanes 会进入到 beginWork 调和阶段,但是 B 组件本身 props 不会发生变化。
情况二:通过 useMemo 等方式缓存了 React element 元素,在渲染控制章节讲到过。
情况三:就是更新发生在当前组件本身,比如 B 组件发生更新,但是 B 组件的 props 并没有发生变化,所以也会走到这个流程上来。
反之如果两者不想等,证明父级 fiber 重新 rerender 导致了 props 改变,此时 didReceiveUpdate = true ,那么第一阶段完成,进入到第二阶段。
新老 props 相等的处理逻辑如下:
- checkScheduledUpdateOrContext
// react-reconciler/src/ReactFiberBeginWork.new.js -> checkScheduledUpdateOrContext
function checkScheduledUpdateOrContext(current,renderLanes){
const updateLanes = current.lanes;
/* 这种情况说明当前更新 */
if (includesSomeLane(updateLanes, renderLanes)) {
return true;
}
/* 如果该 fiber 消费了 context ,并且 context 发生了改变。 */
if (enableLazyContextPropagation) {
const dependencies = current.dependencies;
if (dependencies !== null && checkIfContextChanged(dependencies)) {
return true;
}
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
当新老 props 相等情况,首先会检查当前 fiber 的 lane
是否等于当前的更新优先级,如果相等,那么证明更新来源当前 fiber,比如 B 组件发生更新,那么会走这里(情况三)。当然期间也会判断是否有消费 context
并发生了变化。最后返回状态 hasScheduledUpdateOrContext 。
如果 hasScheduledUpdateOrContext
为 false,证明当前组件没有更新,也没有 context 上的变化,那么还有一种情况就是 child 可能有更新,但是当前 fiber 不需要更新(情况一)。那么会直接返回 attemptEarlyBailoutIfNoScheduledUpdate
,退出第二阶段。
- attemptEarlyBailoutIfNoScheduledUpdate
attemptEarlyBailoutIfNoScheduledUpdate 这个函数会处理部分 Context 逻辑,但是最重要的是调用了 bailoutOnAlreadyFinishedWork
。
// react-reconciler/src/ReactFiberBeginWork.new.js -> bailoutOnAlreadyFinishedWork
function bailoutOnAlreadyFinishedWork(current,workInProgress,renderLanes){
/* 如果 children 没有高优先级的任务,说明所有的 child 都没有更新,那么直接 返回,child 也不会被调和 */
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
/* 这里做了流程简化 */
return null
}
/* 当前fiber没有更新。但是它的children 需要更新。 */
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
2
3
4
5
6
7
8
9
10
11
bailoutOnAlreadyFinishedWork 流程非常重要。它主要做了两件事:
- 首先通过 includesSomeLane 判断 childLanes 是否是高优先级任务,如果不是,那么所有子孙 fiber 都不需要调和 ,那么直接返回 null,child 也不会被调和。
- 如果 childLanes 优先级高,那么证明 child 需要被调和,但是当前组件不需要,所以会克隆一下 children,返回 children ,那么本身不会
rerender
。
# 第二阶段(mount)
第二阶段就是更新 fiber,比如是函数组件,就会调用 updateFunctionComponent
,类组件就调用 updateClassComponent
,然后进行 rerender 了。
对于我们常见的组件类型,如(FunctionComponent
/ClassComponent
/HostComponent
),最终会进入reconcileChildren
方法。
- 对于
mount
的组件,他会创建新的子Fiber节点
- 对于
update
的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点
比较(也就是俗称的Diff
算法),将比较的结果生成新Fiber节点
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 对于mount的组件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 对于update的组件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
从代码可以看出,和beginWork
一样,他也是通过current === null ?
区分mount
与update
。不论走哪个逻辑,最终他会生成新的子Fiber节点
并赋值给workInProgress.child
,作为本次beginWork
返回值,并作为下次performUnitOfWork
执行时workInProgress
的传参。
# 流程总结
接下来以上述中的组件B为例子,在强化一下更新流程。
场景一:当更新 A 时候,那么 A 组件的 fiber 会进入调和流程,会执行 render 形成新的组件 B 对应的 element 元素,接下来调和 B ,因为 B 的 newProps
不等于 oldProps
,所以会 didReceiveUpdate = true
,然后更新组件,也会触发 render。(这里都是默认没有渲染控制的场景,比如 memo
PureComponent
等 )。
场景二:当更新 B 时候,那么 A 组件会标记 childLanes,所以 A 会被调和,但是不会 render,然后到了主角 B ,B 由于新老 props 相等,所以会 checkScheduledUpdateOrContext
流程,判断 lane 等于 renderLanes ,检查到 lane 等于 renderLane,所以会执行更新,触发 render。 C 组件也就跟着更新。
场景三:当更新 C 时候,那么 A 和 B 组件会标记 childLanes,所以 A 和 B 会被调和,但是不会更新,然后到 C ,C 会走正常流程。
场景四:还有一种情况,什么时候 B 会跳出调和流程呢。
# beginWork 流程图
组件更新和调和过程。rerender 一定会调和,但是调和并不一定 rerender,也有可能找到待更新的子元素。