参考文章:React技术揭秘——beginWork
需要先看:
React 中的双缓冲机制
ReactDOM.render 串联渲染链路 —— render 阶段“递归”概览
截止到初始化阶段的时候,我们的 Fiber 树现在仅有一个根节点 rootFiber
所以首先从rootFiber
开始向下深度优先遍历。为遍历到的每个Fiber节点
调用 beginWork方法
由于整个 beginWork 模块代码量太多,先进行概览然后分几步解读
逻辑概览(react 版本 v17.0.1)
function beginWork(
current: Fiber | null, // 当前组件在页面渲染的Fiber节点
workInProgress: Fiber, // 内存中构建的 workInProgress Fiber 树的
renderLanes: Lanes // 优先级相关,和 Scheduler 有关
): 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:
// ...省略
// ...省略其他类型
}
}
从传参看
function beginWork(
current: Fiber | null, // 当前组件在页面渲染的Fiber节点
workInProgress: Fiber, // 内存中构建的 workInProgress Fiber 树的 Fiber节点
renderLanes: Lanes, // 优先级相关,和 Scheduler 有关
): Fiber | null {
// ...省略函数体
}
传参详情:
- current:当前组件在页面对应的Fiber节点,也是 workInProgress.alternate
- workInProgress: 当前组件在内存中构建的Fiber节点,也是 current.alternate
- renderLanes:优先级相关,与 Scheduler 有关
在React 中的双缓冲机制有介绍:
在mount阶段,由于首次渲染,整个 Fiber 树只有一个 rootFiber(rootFiber 对应的是虚拟DOM的根节点和实际DOM中的root节点对应)。不存在当前组件对应的 Fiber 节点(如 App 节点此时也是不存在的) 。即 mount 时 current === null.
而由于组件要 update时,由于之前已经 mount 过了,整个 Fiber 树已经渲染到页面上去了,所以 current !== null
基于此原因,beginWork 的工作可以分为两部分:
- mount 时:除了 rootFiber (fiberRootNode的实例) 以外,组件的Fiber节点都不存在,即 current === null。会根据 fiber.tag 不同,创建不同类型的子Fiber节点。
- update 时:current !== null,这个时候 workInProgress Fiber 树就可以考虑复用 current 节点。
update 时
进入 current !== null
还有一个有关didReceiveUpdate
是否为true
的逻辑(为 false 则节点可复用,为 ture 不可复用):
-
workInProgress.type !== current.type
(fiber.type变了,不可复用) -
oldProps !== newProps
(props改变,不可复用) -
hasContextChanged()
返回值为 true (看名字估计是 Context 有变也不可复用) -
!includesSomeLane(renderLanes, updateLanes)
为 false(当前节点优先级不够,不可复用) -
(current.flags & ForceUpdateForLegacySuspense) !== NoFlags
为真,不可复用(看源码注释这个判断是后来为了修复一些好像是只存在于 legacy mode的特例情况,有关此代码的详情可以戳这里)
注:legacy mode 就是当前使用的 ReactDOM.render 启动的模式,也就是所谓的遗留模式
源码逻辑概览:
if (current !== null) {
var oldProps = current.memoizedProps;
var newProps = workInProgress.pendingProps;
if (oldProps !== newProps || hasContextChanged() || (
workInProgress.type !== current.type )) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
switch (workInProgress.tag) {
//....一堆case
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
didReceiveUpdate = true;
} 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
这个方法里面代码很简单:
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
// current为空,即处于mount阶段,
//先在内存中构建 workInProgress Fiber 树
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
// current 不为空,即处于update阶段,
// 同样也先构建 workInProgress Fiber 树,但此时可以考虑复用 rootFiber 下的 Fiber树节点
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
}
}
代码上看,也是通过 current === null ?
区分 mount
与 update
.
不论走哪个逻辑,最终他会生成新的子Fiber节点
并赋值给workInProgress.child
,作为本次beginWork
返回值,并作为下次performUnitOfWork
执行时workInProgress
的传参。
简单翻译一下就是,workInProgress 得到 Fiber 子节点后,继续从该子 Fiber 节点往下生成 workInProgress Fiber 树.
注:值得一提的是,
mountChildFibers
与reconcileChildFibers
这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers
会为生成的 Fiber 节点带上effectTag
属性,而mountChildFibers
不会。同时reconcileChildFibers
会考虑是否需要复用节点,此时就利用到了 Diffing 算法。
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
。
这个时候有个问题,上面刚刚说过了,mount 时,reconcileChildren
调用了 mountChildFibers
且这个方法不会为 Fiber 节点赋值effectTag
。那首屏渲染时如何将未有的节点插入进页面的呢?
假设 mountChildFibers
也会赋值 effectTag
,那么可以预见 mount 时整棵 Fiber 树所有节点的 effectTag
值都为 Placement
。那么 commit 阶段在执行 DOM 操作时每个节点都会执行一次插入操作,这样大量的 DOM 操作是极低效的。但是,别忘了我们还有一个 rootFiber,所以在 mount 时,只要给 rootFiber 的 effectTag
赋值 Placement
,在 commit 阶段只会执行一次插入操作。
网友评论