React源码解析————Render阶段(二)
2021SC@SDUSC
2021SC@SDUSC
BeginWork
我们先不看源码,先看传递的参数
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
)
1.current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,workInProgress.alternate
2.workInProgress:当前组件对应的Fiber节点
3.renderLanes:优先级相关,在讲解Scheduler时再讲解
组件mount时,由于是首次渲染,是不存在当前组件对应的Fiber节点在上一次更新时的Fiber节点,即mount时current 为 null。组件update时,由于之前已经mount过,所以current !== null。所以我们可以通过判断current是否为null来判断组件是处于mount还是update。
同样基于此原因:
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 {
if (__DEV__) {
if (workInProgress._debugNeedsRemount && current !== null) {
// This will restart the begin phase with a new fiber.
return remountFiber(
current,
workInProgress,
createFiberFromTypeAndProps(
workInProgress.type,
workInProgress.key,
workInProgress.pendingProps,
workInProgress._debugOwner || null,
workInProgress.mode,
workInProgress.lanes,
),
);
}
}
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
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;
} else {
// Neither props nor legacy context changes. Check if there's a pending
// update or context change.
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
// If this is the second pass of an error or suspense boundary, there
// may not be work scheduled on `current`, so we check for this flag.
(workInProgress.flags & DidCapture) === NoFlags
) {
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// This is a special case that only exists for legacy mode.
// See https://github.com/facebook/react/pull/19216.
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;
}
// Before entering the begin phase, clear pending update priority.
// TODO: This assumes that we're about to evaluate the component and process
// the update queue. However, there's an exception: SimpleMemoComponent
// sometimes bails out later in the begin phase. This indicates that we should
// move this assignment out of the common path and into each branch.
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes,
);
}
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 HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
case ForwardRef: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === type
? unresolvedProps
: resolveDefaultProps(type, unresolvedProps);
return updateForwardRef(
current,
workInProgress,
type,
resolvedProps,
renderLanes,
);
}
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
case Mode:
return updateMode(current, workInProgress, renderLanes);
case Profiler:
return updateProfiler(current, workInProgress, renderLanes);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
const outerPropTypes = type.propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
resolvedProps, // Resolved for outer only
'prop',
getComponentNameFromType(type),
);
}
}
}
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
renderLanes,
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
renderLanes,
);
}
case IncompleteClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return mountIncompleteClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case SuspenseListComponent: {
return updateSuspenseListComponent(current, workInProgress, renderLanes);
}
case ScopeComponent: {
if (enableScopeAPI) {
return updateScopeComponent(current, workInProgress, renderLanes);
}
break;
}
case OffscreenComponent: {
return updateOffscreenComponent(current, workInProgress, renderLanes);
}
case LegacyHiddenComponent: {
return updateLegacyHiddenComponent(current, workInProgress, renderLanes);
}
case CacheComponent: {
if (enableCache) {
return updateCacheComponent(current, workInProgress, renderLanes);
}
break;
}
}
invariant(
false,
'Unknown unit of work tag (%s). This error is likely caused by a bug in ' +
'React. Please file an issue.',
workInProgress.tag,
);
}
通过浏览代码,我们可以很明显的看到,大部分的beginwork的代码都是根据各种tag的不同来创建不同的子Fiber节点。
对于我们常见的组件类型,如(FunctionComponent/ClassComponent/HostComponent),最终会进入reconcileChildren方法。
reconcileChildren
从该函数名就能看出这是Reconciler模块的核心部分。那么他究竟做了什么?
1.对于mount的组件,他会创建新的子Fiber节点
2.对于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的传参。
注意:
值得一提的是,mountChildFibers与reconcileChildFibers这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers会为生成的Fiber节点带上effectTag属性,而mountChildFibers不会。
EffectTag
render阶段的工作是在内存中进行,当工作结束后会通知Renderer需要执行的DOM操作。要执行DOM操作的具体类型就保存在fiber.effectTag中。
export type SideEffectTag = number;
// Don't change these two values. They're used by React Dev Tools.
export const NoEffect = /* */ 0b000000000000000;
export const PerformedWork = /* */ 0b000000000000001;
// You can change the rest (and add more).
// DOM需要插入到页面中
export const Placement = /* */ 0b000000000000010;
//DOM需要更新
export const Update = /* */ 0b000000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /* */ 0b000000000000110;
// DOM需要删除
export const Deletion = /* */ 0b000000000001000;
export const ContentReset = /* */ 0b000000000010000;
export const Callback = /* */ 0b000000000100000;
export const DidCapture = /* */ 0b000000001000000;
export const Ref = /* */ 0b000000010000000;
export const Snapshot = /* */ 0b000000100000000;
export const Passive = /* */ 0b000001000000000;
export const PassiveUnmountPendingDev = /* */ 0b010000000000000;
export const Hydrating = /* */ 0b000010000000000;
export const HydratingAndUpdate = /* */ 0b000010000000100;
// Passive & Update & Callback & Ref & Snapshot
export const LifecycleEffectMask = /* */ 0b000001110100100;
// Union of all host effects
export const HostEffectMask = /* */ 0b000011111111111;
// These are not really side effects, but we still reuse this field.
export const Incomplete = /* */ 0b000100000000000;
export const ShouldCapture = /* */ 0b001000000000000;
export const ForceUpdateForLegacySuspense = /* */ 0b100000000000000;
// Union of side effect groupings as pertains to subtreeTag
export const BeforeMutationMask = /* */ 0b000001100001010;
export const MutationMask = /* */ 0b000010010011110;
export const LayoutMask = /* */ 0b000000010100100;
这里通过二进制表示effectTag,可以方便的使用位操作为fiber.effectTag赋值多个effect。
那么,如果要通知Renderer将Fiber节点对应的DOM节点插入页面中,需要满足两个条件:
1.fiber.stateNode存在,即Fiber节点中保存了对应的DOM节点
2.(fiber.effectTag & Placement) !== 0,即Fiber节点存在Placement effectTag
而我们知道,mount时,fiber.stateNode === null,且在reconcileChildren中调用的mountChildFibers不会为Fiber节点赋值effectTag。那么首屏渲染如何完成呢?
假设mountChildFibers也会赋值effectTag,那么可以预见mount时整棵Fiber树所有节点都会有Placement effectTag。那么commit阶段在执行DOM操作时每个节点都会执行一次插入操作,这样大量的DOM操作是极低效的。为了解决这个问题,在mount时只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。
针对fiber.stateNode === null这个问题,fiber.stateNode会在completeWork中创建,我们会在下一节介绍。
我们回来继续看beginWork,当然还有:在current!==null 时,即update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
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) {
// 省略处理
}
//复用current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = false;
}
我们可以看到,满足如下情况时didReceiveUpdate === false(即可以直接复用前一次更新的子Fiber,不需要新建子Fiber)
1.oldProps = newProps && workInProgress.type = current.type,即props与fiber.type不变
2.!includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够,会在讲解Scheduler时介绍
图片来源于网络,作者看到请联系我,我会加上出处