Appearance
beginWork(FiberNode更新入口)
这个在React的源码中是一个单独的文件 react-reconciler/src/ReactFiberBeginWork.new.js
这个阶段的主要特点是:
- 产生 current (没有current的说明是初次渲染,然后在beginWork的流程通过workInProgree生成FiberNode,并存在alternate上,作为下次的current )
- 根据 lanes 和 childLanes 生成FiberNode
其主要作用是:根据组件类型的不同生成对应的FiberNode对象,并在update的过程判断原FiberNode是否可以复用
其主要分为两个部分
- 根据各个条件判断是否可以复用(didReceiveUpdate )
- 根据 workInProgress.tag 调用对应组件类型的处理方法生成FiberNode
栗子
js
const Comp211 = props => {
return <div>this is Comp211</div>
}
const Comp221 = props => {
const [num, setNumber] = React.useState(1)
const handleClickBtn = () => {
setNumber(preNumber => preNumber + 1)
}
return (
<div>
this is Comp221
<div>
<button onClick={handleClickBtn}>点击更新 - {num}</button>
</div>
<Comp2211 />
</div>
)
}
const Comp2211 = props => {
return <div>this is Comp2211</div>
}
const Comp21 = props => {
return (
<div>
this is Comp21
<Comp211 />
</div>
)
}
const Comp22 = props => {
return (
<div>
this is Comp22
<Comp221 />
</div>
)
}
const Comp23 = props => {
return <div>this is Comp23</div>
}
const App = props => {
return (
<div>
<Comp21 />
<Comp22 />
<Comp23 />
</div>
)
}
如上述栗子,我们在组件Comp221点击了一个点击事件,从而触发了一个Update,那么在beginWork的之前 performUnitOfWork中,会在const root = markUpdateLaneFromFiberToRoot(fiber, lane)
的时候,1. 通过Comp221组件的return找到跟FiberRoot; 2. 在向上寻找的过程中会标记Comp221组件的 lanes 和 祖先组件的 childLanes。结果如下图(注意: 在任务调度的时候 可能会有多个更新,那么也就可能存在多个更新的线路)
FiberNode 复用和强制更新
我们看一下判断条件
js
// --------------- 判断是否需要强制更新 ---------------------
// current !== null 就是组件不是初次渲染
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
// 如果组件的 1. props 改变了 2. context 改变了 3. 组件类型不同
// 那么组件就不能复用, 需要强制更新了
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// Force a re-render if the implementation changed due to hot reload:
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
// 判断组件上的优先级是否包含当前渲染的优先级
// 如某一个子组件触发了 更新如 setState 那么这个组件的 Lanes 就会包含这个优先级 即尾位为1
// 当前渲染的通道为 renderLanes 如 000000000000xxx0001
// 这时候就是包含,跳过
// 对于上级节点或者其他节点等 lanes 可能为 0000000 , 这时候不包含,那说明这个节点不需要重新render
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
switch (workInProgress.tag) { ... }
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
didReceiveUpdate = true;
} else {
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
}
从上面找到组件复用的条件就只有一点点 return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
可见对于FiberNode的复用的条件还是很多的
current
在ReactDOM.render流程中,会在render的renderRootSync下prepareFreshStack初始化跟的workInProgress 并将原FiberNode指向workInProgress.alternal,到performUnitOfWork的时候会将这个作为current。
那么看见 current就是代表原来的 FiberNode对象
上文 if(current === null ){ didReceiveUpdate = false; } 说明此组件是初始渲染,肯定走创建流程
可见对于beginWork其又可以分为主要的两个阶段:向下遍历 和 向上回溯 的过程。
bailoutOnAlreadyFinishedWork( current , workInProgress , renderLanes )( 组件复用方法 )
我们先简单看一下组件复用的代码
js
/**
* 跳过当前节点的render
* - 对于 childLanes 包含的 renderLanes , 就会进行 cloneChildFibers, 进行子节点的 beginWork
* - 对于 childLanes 都不包含 当前renderLanes ,直接跳过
* @param {*} current
* @param {*} workInProgress
* @param {*} renderLanes
* @returns
*/
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null;
} else {
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
// 虽然这个节点可以复用,但是其子节点不一定是可以复用的,所以需要clone子节点 并向下继续render
// 主要作用于 有更新组件这条线上面的组件,他们本身可以复用,但是子节点不一定可以复用了
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
}
可见在**bailoutOnAlreadyFinishedWork( current , workInProgress , renderLanes )
的逻辑是非常简单的,那么结合上述例子和 beginWork中进入bailoutOnAlreadyFinishedWork
的判断条件我们就可以大体了解 FiberNode 复用的条件了 **
首先我们不考虑多更新线路,多优先级的情况下,我们可以将上述FiberNode树分为以下类型
- 不是Update更新祖先线路上的其他分支节点Node(如Comp21、Comp23)
- Update更新祖先节点Node(其childLanes === 0b000001)(如App等)
- Update节点本身(Comp22)(其lanes === 0b00001)
- Update节点子孙节点
下面就是围绕这四种类型的节点去区分 重新生成 、节点及其子孙全都复用、节点本身复用(子孙继续处理)
oldProps !== newProps || hasLegacyContextChanged() 组件的** Props不同 **或者 **上下文Context改变 **了那么肯定不能复用,直接
didReceiveUpdate 强制更新
!includesSomeLane(renderLanes, updateLanes)
优先级不同也直接跳过(updateLanes === FiberNode.lanes)
在一次任务调度过程中可能存在多个优先级的更新,那么对于其他优先级的更新功能肯定不做处理了
除UpdateNode本身其他的节点也会进入到
bailoutOnAlreadyFinishedWork( current , workInProgress , renderLanes )的流程
但是具体是 节点及其子孙全都复用、节点本身复用,需要再判断
!includesSomeLane(renderLanes, workInProgress.childLanes)
这就是判断 节点及其子孙全都复用、节点本身复用
在**
bailoutOnAlreadyFinishedWork
**中通过判断了 childLanes ,如上述例子- 如果不在当前更新的祖先节点上,那么就可以直接 节点及其子孙全都复用
- 如果是在当前更新的祖先节点上,那么节点本身可以复用,其子孙节点能不能复用需要再次判断(加入到workInProgress进入循环)。即节点本身复用
(current.flags & ForceUpdateForLegacySuspense) !== NoFlags
TODO: 待分析
在scheduleUpdateOnFiber 的 markUpdateLaneFromFiberToRoot(fiber, lane)方法中对于触发Update的组件会将其FiberNode.lanes上标记触发更新的优先级。
如果我们只判断组件的 props 和 上下文context 没有更新就直接复用FiberNode,那么对于触发更新本身就不更新了(错误),所以我们还需要添加一个如果存在当前优先级的更新标记,那么也需要重新创建
** FiberNode复用的过程( cloneChildFibers(current, workInProgress) )**
从上述可以看出进入到这个过程的主要是 UpdateNode的祖先节点(Node)
js
export function cloneChildFibers(
current: Fiber | null,
workInProgress: Fiber,
): void {
invariant(
current === null || workInProgress.child === current.child,
'Resuming work not yet implemented.',
);
// 没有子节点的情况。 返回null,从而进入 completeWork 过程
if (workInProgress.child === null) {
return;
}
let currentChild = workInProgress.child;
// 创建子节点的workInProgress
let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
// child 与 return 的相互绑定
workInProgress.child = newChild;
newChild.return = workInProgress;
// 处理兄弟节点
while (currentChild.sibling !== null) {
currentChild = currentChild.sibling;
newChild = newChild.sibling = createWorkInProgress(
currentChild,
currentChild.pendingProps,
);
newChild.return = workInProgress;
}
newChild.sibling = null;
}
可以看出 cloneChildFibers 也没有啥Diff算法的,就是创建子节点及其兄弟的 workInProgress,并与当前workInProgress进行child和return的相互绑定
总结
从上述流程总结得出 render的beginWork的过程,其主要是对FiberNode的新建、复用过程。
通过 current 和 workInProgress 去维护两条FiberNode树
根据深度优先的规则,通过修改当前workInProgress 的指向,不断形成新的FiberNode树
通过在
const root = markUpdateLaneFromFiberToRoot(fiber, lane)
的过程标记的 lanes 和 childLanes,从而区分1. 当前UpdateNode 2. 当前UpdateNode祖先节点 3. 当前UpdateNode祖先的其他分支节点(当然包含子孙节点)在 beginWork的时候通过 props、context、lanes、childLanes来区分节点是否可以复用
对于可以复用的节点,通过是否是分支节点或者更新的子孙节点(props和context没变化的),从而直接复用整个节点树,避免了其子孙节点的beginWork和completeWork的过程