递归阶段beginWork
上一节我们了解到render阶段
的工作可以分为“递”阶段和“归”阶段。其中“递”阶段会执行beginWork
,“归”阶段会执行completeWork
。这一节我们看看“递”阶段的beginWork
方法究竟做了什么。
方法概览
可以从源码这里看到beginWork
的定义。整个方法大概有500行代码。
从上一节我们已经知道,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 {
// update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
if (current !== null) {
// ...省略
// 复用current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}
// mount时:根据tag不同,创建不同的子Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
}
update时
我们可以看到,满足如下情况时didReceiveUpdate === false
(即可以直接复用前一次更新的子Fiber
,不需要新建子Fiber
)
oldProps === newProps && workInProgress.type === current.type
,即props
与fiber.type
不变!includesSomeLane(renderLanes, updateLanes)
,即当前Fiber节点
优先级不够,会在讲解Scheduler
时介绍
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
switch (workInProgress.tag) {
// 省略处理
}
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = false;
}
mount时
当不满足优化路径时,我们就进入第二部分,新建子Fiber
。
我们可以看到,根据fiber.tag
不同,进入不同类型Fiber
的创建逻辑。
可以从这里看到
tag
对应的组件类型
// mount时:根据tag不同,创建不同的Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
对于我们常见的组件类型,如(FunctionComponent
/ClassComponent
/HostComponent
),最终会进入reconcileChildren方法。
reconcileChildren
从该函数名就能看出这是Reconciler
模块的核心部分。那么他究竟做了什么呢?
对于
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,
);
}
}
从代码可以看出,和beginWork
一样,他也是通过current === null ?
区分mount
与update
。
不论走哪个逻辑,最终他会生成新的子Fiber节点
并赋值给workInProgress.child
,作为本次beginWork
返回值,并作为下次performUnitOfWork
执行时workInProgress
的传参。
::: warning 注意
值得一提的是,mountChildFibers
与reconcileChildFibers
这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers
会为生成的Fiber节点
带上effectTag
属性,而mountChildFibers
不会。
:::
effectTag
我们知道,render阶段
的工作是在内存中进行,当工作结束后会通知Renderer
需要执行的DOM
操作。要执行DOM
操作的具体类型就保存在fiber.effectTag
中。
你可以从这里看到
effectTag
对应的DOM
操作
比如:
// DOM需要插入到页面中
export const Placement = /* */ 0b00000000000010;
// DOM需要更新
export const Update = /* */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /* */ 0b00000000000110;
// DOM需要删除
export const Deletion = /* */ 0b00000000001000;
通过二进制表示
effectTag
,可以方便的使用位操作为fiber.effectTag
赋值多个effect
。
那么,如果要通知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阶段
只会执行一次插入操作。
::: details 根Fiber节点 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
逻辑
:::
参考资料
beginWork
流程图