Appearance
上一节我们了解到render阶段
的工作可以分为“递”阶段和“归”阶段。其中“递”阶段会执行beginWork
,“归”阶段会执行completeWork
。这一节我们看看“递”阶段的beginWork
方法究竟做了什么。
方法概览
可以从源码这里看到beginWork
的定义。整个方法大概有500行代码。
从上一节我们已经知道,beginWork
的工作是传入当前Fiber节点
,创建子Fiber节点
,我们从传参来看看具体是如何做的。
从传参看方法执行
js
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节点
js
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
时介绍
js
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
对应的组件类型
js
// 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节点
js
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
的传参。
注意
值得一提的是,mountChildFibers
与reconcileChildFibers
这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers
会为生成的Fiber节点
带上effectTag
属性,而mountChildFibers
不会。
effectTag
我们知道,render阶段
的工作是在内存中进行,当工作结束后会通知Renderer
需要执行的DOM
操作。要执行DOM
操作的具体类型就保存在fiber.effectTag
中。
你可以从这里看到
effectTag
对应的DOM
操作
比如:
js
// 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阶段
只会执行一次插入操作。
根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
流程图