Reconciler阶段概览
Reconciler
阶段就是调和的过程,即:
- 采用深度优先遍历算法,生成fiber结构,构建对应的workInProgress树,
- 找到diff的
- 根据优先级调度任务
- 根据节点态生成一个Effect List
通过前面章节我们知道,在render方法中调用updateContainer,沿着这条调用链接
updateContainer -> performSyncWorkOnRoot -> renderRootSync -> workLoopSync ->performUnitOfWork
那么render阶段开始于performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新。
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
可以看到,他们唯一的区别是是否调用shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历。
workInProgress代表当前已创建的workInProgress fiber。
performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树。
我们知道Fiber Reconciler是从Stack Reconciler重构而来,通过遍历的方式实现可中断的递归,所以performUnitOfWork的工作可以分为两部分:“递”和“归”。
递阶段-beginWork
首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork方法(opens new window)。
该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。
当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。
归阶段-completeWork
在“归”阶段会调用completeWork(opens new window)处理Fiber节点。
当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。
如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。
“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了。
demo
function App() {
return (
<div className="App">
<h3>
<p>hello React source code~</p>
</h3>
<span>Joy Guan</span>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
对应fiber树
分别在源码中的递和归阶段打log
其中的tag 定义见 tag定义
tag为3说明是HostRoot,即HostRootFiber
tag为2是IndeterminateComponent 决定是函数式组件还是类组件
tag5为5是HostComponent
由打印结果可以清晰看到 递-归过程。
递-beginWork
beginWork 的工作是传入 当前Fiber节点,创建对应的 子Fiber节点。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...省略函数体
}
其中传参:
- current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
- workInProgress:当前组件对应的Fiber节点
- renderLanes:优先级相关,在讲解Scheduler时再讲解
除rootFiber以外, 组件mount时,由于是首次渲染,是不存在当前组件对应的Fiber节点在上一次更新时的Fiber节点,即mount时current === null。
组件update时,由于之前已经mount过,所以current !== null。
所以我们可以通过current === null ?来区分组件是处于mount还是update。
基于此原因,beginWork的工作可以分为两部分:
- update时:如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child。
- mount时:除fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
// workInProgress:当前组件对应的Fiber节点
// renderLanes:优先级相关,在讲解Scheduler时再讲解
//更新阶段
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged()
) {
// props和context没有变化
didReceiveUpdate = true;
} else {
// 检查是否存在正在更新的Context
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
(workInProgress.flags & DidCapture) === NoFlags
) {
// didReceiveUpdate === false(即可以直接复用前一次更新的子Fiber,不需要新建子Fiber)
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
didReceiveUpdate = true;
} else {
// An update was scheduled on this fiber, but there are no new props
// nor legacy context. Set this to false. If an update queue or context
// consumer produces a changed value, it will set this to true. Otherwise,
// the component will assume the children have not changed and bail out.
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
}
workInProgress.lanes = NoLanes;
// mount时:根据tag不同,创建不同的子Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
renderLanes,
);
}
}
// ...省略其他类型
}
可以看到,不同类型组件对应不同的处理,我们常见的classComponent、fucntionComponent、memoCompoent分别调用了updateComonent/updateFunctionComponent/upddateMemoComponnet
最终会进入reconcileChildren(opens new window)方法。
reconcilerChildren
reconcilerChildren做了什么呢?
- 对于mount组件,创建fiber节点
对于update组件,对比当前组件和改组件在上次更新时对应的fiber节点 做比较(diff),将diff的结果生成新的fiber节点 ```javascript 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,
); } }
mount和update分别调用mountChildFibers和reconcilerChildFIbers,最终会进入diff阶段(ChildReconciler),diff阶段分析详见 [diff算法详解](https://www.yuque.com/guanguan-ky3w9/cf85a3/rqwl65)
不论是mount还是update,最终他会生成新的子Fiber节点并赋值给workInProgress.child,作为本次beginWork返回值,并作为下次performUnitOfWork执行时workInProgress的传参。
> 值得一提的是,mountChildFibers与reconcileChildFibers这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers会为生成的Fiber节点带上effectTag属性,而mountChildFibers不会。
effectTag类型有
```javascript
// DOM需要插入到页面中
export const Placement = /* */ 0b00000000000010;
// DOM需要更新
export const Update = /* */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /* */ 0b00000000000110;
// DOM需要删除
export const Deletion = /* */ 0b00000000001000;
我们知道 render阶段是在内存中进行的,render结束后要通知 Renderer 根据effectTag执行对应的DOM操作,
那么,如果要通知Renderer将fiber节点 对应的DOM节点 插入到页面中,需要满足:
- fiber.stateNode存在,即Fiber节点中保存了对应的DOM节点
- (fiber.effectTag & Placement) !== 0,即Fiber节点存在Placement effectTag
我们知道,mount时,fiber.stateNode === null,且在reconcileChildren中调用的mountChildFibers不会为Fiber节点赋值effectTag。那么首屏渲染如何完成呢?
针对第一个问题,fiber.stateNode会在completeWork中创建,我们会在下一节介绍。
第二个问题的答案十分巧妙:假设mountChildFibers也会赋值effectTag,那么可以预见mount时整棵Fiber树所有节点都会有Placement effectTag。那么commit阶段在执行DOM操作时每个节点都会执行一次插入操作,这样大量的DOM操作是极低效的。
为了解决这个问题,在mount时只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。
验证DEMO
借用上一节的Demo,第一个进入beginWork方法的Fiber节点就是rootFiber,他的alternate指向current rootFiber(即他存在current)。
为什么rootFiber节点存在current(即rootFiber.alternate),我们在双缓存机制一节mount时的第二步已经讲过
由于存在current,rootFiber在reconcileChildren时会走reconcileChildFibers逻辑。
而之后通过beginWork创建的Fiber节点是不存在current的(即 fiber.alternate === null),会走mountChildFibers逻辑
归-competeWork
上面我们知道组件执行beginWork后会创建子Fiber节点,节点上可能存在effectTag。
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
// ...省略
return null;
}
case HostRoot: {
// ...省略
updateHostContainer(workInProgress);
return null;
}
case HostComponent: {
// ...省略
return null;
}
// ...省略
重点关注页面渲染所必须的HostComponent(即原生DOM组件对应的Fiber节点)
处理HostComponent
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// update的情况
// ...省略
} else {
// mount的情况
// ...省略
}
return null;
}
根据current === null ?判断是mount还是update,同时针对HostComponent,判断update时我们还需要考虑workInProgress.stateNode != null ?(即该Fiber节点是否存在对应的DOM节点)
当update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。
需要做的主要是处理props,比如:
- onClick、onChange等回调函数的注册
- 处理style prop
- 处理DANGEROUSLY_SET_INNER_HTML prop
- 处理children prop
我们去掉一些当前不需要关注的功能(比如ref)。可以看到最主要的逻辑是调用updateHostComponent方法。
workInProgress.updateQueue = (updatePayload: any);
在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。
mount阶段主要逻辑包括三个:
- 为Fiber节点生成对应的DOM节点
- 将子孙DOM节点插入刚生成的DOM节点中
- 与update逻辑中的updateHostComponent类似的处理props的过程 ```javascript // mount的情况
// …省略服务端渲染相关逻辑
const currentHostContext = getHostContext(); // 为fiber创建对应DOM节点 const instance = createInstance( type, newProps, rootContainerInstance, currentHostContext, workInProgress, ); // 将子孙DOM节点插入刚生成的DOM节点中 appendAllChildren(instance, workInProgress, false, false); // DOM节点赋值给fiber.stateNode workInProgress.stateNode = instance;
// 与update逻辑中的updateHostComponent类似的处理props的过程 if ( finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, ) ) { markUpdate(workInProgress); }
上面我们讲到:mount时只会在rootFiber存在 Placement effectTag。那么commit阶段是如何通过一次插入DOM操作(对应一个Placement effectTag)将整棵DOM树插入页面的呢?
原因就在于completeWork中的appendAllChildren方法。
由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到rootFiber时,我们已经有一个构建好的离屏DOM树。
还有一个问题:作为DOM操作的依据,commit阶段需要找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。难道需要在commit阶段再遍历一次Fiber树寻找effectTag !== null的Fiber节点么?<br />这显然是很低效的。
为了解决这个问题,在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中。
effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。<br />类似appendAllChildren,在“归”阶段,所有有effectTag的Fiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。
```javascript
nextEffect nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber
这样,在commit阶段只需要遍历effectList就能执行所有effect了。