前言:最近离职准备面试,把之前写的笔记整理一下发出来,本人能力有限,如有错误的地方尽情指正
博客链接:pionpill
对应源码: github.com/facebook/re…
beginWork 方法的目的是创建新的节点替换原节点,先看一下源代码的大致逻辑(✨约3946行):
function beginWork(
current: Fiber | null, // 界面上的当前节点
workInProgress: Fiber, // 构建中的当前节点
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
// 该组件存在,进行 update 操作
} else {
// 该组件没有,进行 mount 操作
}
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
// 针对 tag 类型进行具体操作
}
// 没 switch 到对应类型,抛错
throw new Error('xxx');
}
首先,从 current 是否存在可以判断出该节点是需要新建(mount)还是更新(update),使用一些变量打上标记供后续处理。然后根据组件类型进行相关处理。
第一次进入 beginWork 工作的节点一定是 HostFiber(根节点)且一定是存在的。
组件更新
组件 update
看一下固有组件 update 的逻辑:
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps || // 这时候会拿新老 props 进行浅比较
hasLegacyContextChanged() // 上下文是否变化
) {
didReceiveUpdate = true; // 需要 update
// 这里搭上了 update 标签但是没有执行相关逻辑,因为后续会根据 memo 判断
} else {
// 如果通过内部状态(context)判断组件是否要更新
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
if (
!hasScheduledUpdateOrContext &&
(workInProgress.flags & DidCapture) === NoFlags
) {
// 进入这里表示不存在内部状态更新,且 FiberNode 也没有被打上任何更新 flag
// 检查是否不是错误或悬挂边界的第二次遍历,如果满足条件复用当前节点(优化)
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
// legacy 模式的特殊情况需要 update,一般都执行下面的 false 赋值逻辑
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
didReceiveUpdate = true;
} else {
didReceiveUpdate = false;
}
}
}
这里我们发现如果发现需要更新并没有立即执行更新逻辑,而是给全局的 didReceiveUpdate 变量赋值。
内部状态更新判断我们看一下 checkScheduledUpdateOrContext 方法:
function checkScheduledUpdateOrContext(
current: Fiber,
renderLanes: Lanes,
): boolean {
// 判断是否有等待更新的内容
const updateLanes = current.lanes;
if (includesSomeLane(updateLanes, renderLanes)) {
return true;
}
// 在懒加载 context 模式下,判断依赖的 context 是否有变化
if (enableLazyContextPropagation) {
const dependencies = current.dependencies;
if (dependencies !== null && checkIfContextChanged(dependencies)) {
return true;
}
}
return false;
}
attemptEarlyBailoutIfNoScheduledUpdate 方法会在没有更新计划时尝试提前退出并复用当前的 Fiber 节点,针对不同的 tag 类型节点,它会对子节点,context 做一些操作以确保是否有相关节点需要变更,最终它会调一个名为 bailoutOnAlreadyFinishedWork 的方法并作为返回值(✨约3594行):
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
workInProgress.dependencies = current.dependencies;
}
markSkippedUpdateLanes(workInProgress.lanes);
// 判断子节点是否存在任务
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
if (enableLazyContextPropagation && current !== null) {
// 判断子节点是否有 context 变更
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null; // 返回 null 表示子节点不需要变更
}
} else {
return null;
}
}
// 到这里说明子节点需要变更
cloneChildFibers(current, workInProgress);
// 如果返回了新建的子节点,代表子节点不满足优化策略,需要更新
return workInProgress.child;
}
源码(✨约1786行):
export function cloneChildFibers(
current: Fiber | null,
workInProgress: Fiber,
): void {
if (workInProgress.child === null) {
return;
}
let currentChild = workInProgress.child;
// 这个方法创建一个新的 FiberNode 并进行属性赋值操作
let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
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;
}
这里先回顾一下触发 react 组件更新的方式:
- 传入组件的 props 改变
- 组件订阅的 context 改变
- 调用了
forceUpdate方法(逐渐废弃)
对应的上面执行过程中
- 首先会通过
oldProps !== newProps对 props 进行浅比较,如果有新的 props 则强制更新,注意这里是浅比较,因此即使 props 内容相同引用不同,react 组件也会更新。 - 其次通过
checkScheduledUpdateOrContext查询组件是否有需要更新的内部状态。 - 最后会判断子节点是否需要变更,如果需要则克隆直接子节点。
组件 mount
看一下 mount 阶段的逻辑:
else {
// 这里标识不是 update 操作
didReceiveUpdate = false;
// 对应 children 为列表的情形,进行注水操作
// 具体来说是为每个子组件生成唯一的树 ID
if (getIsHydrating() && isForkedChild(workInProgress)) {
const slotIndex = workInProgress.index;
const numberOfForks = getForksAtLevel(workInProgress);
pushTreeId(workInProgress, numberOfForks, slotIndex);
}
}
新 FiberNide 挂载比较简单,只对多 child 模式做了优化: 打上 key, 生成 id。
具体操作
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
// 针对不同的 tag 类型,进行更新或挂载,这里只看一个最常见的函数组件
// 详细的 tag 类型(memo, ref) 我们会在讲 hook 的时候讲
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 xxx: {
// 略
}
}
// 没 switch 到对应类型,抛错
throw new Error('xxx');
这里根据组件的类型不同从 workInProgress 上取属性,并调用对应的 updateXXX 或 mountXXX 对组件进行操作。
reconcileChildren
对于常见的组件类型,最终会调用一个名为 reconcileChildren 方法(✨约329行):
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,
);
}
}
这两个方法其实是调的一个方法:
export const reconcileChildFibers: ChildReconciler = createChildReconciler(true);
export const mountChildFibers: ChildReconciler = createChildReconciler(false);
这个 createChildReconciler 有点小长(1200+行),包含了针对各种子节点的各种操作(创建,更新,替换)。有兴趣的话可以自行看一下源代码,这里接收的布尔类型参数表示是否要跟踪副作用,我们只看一下里面的 reconcileChildFibers 方法。
reconcileChildFibers
reconcileChildFibers 核心逻辑在 reconcileChildFibersImpl 中(✨约1601行):
function reconcileChildFibersImpl(
returnFiber: Fiber, // 父节点
currentFirstChild: Fiber | null, // 子节点
newChild: any, // 新的 child 内容
lanes: Lanes,
): Fiber | null {
// 处理 fragment 片段,当作 array
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
if (typeof newChild === 'object' && newChild !== null) {
// 单节点处理
switch (newChild.$$typeof) {
// 类组件,函数组件都属于此类别
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes),
);
// 省略 ...
}
// 数组节点处理,循环创建多个子节点返回第一个子节点
if (isArray(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
}
// 迭代函数,例如 map 处理
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, lanes);
}
// 是可用节点(我没遇到过这种情形)
if (typeof newChild.then === 'function') {
const thenable: Thenable<any> = (newChild: any);
return reconcileChildFibersImpl(returnFiber, currentFirstChild, unwrapThenable(thenable), lanes);
}
// 上下文节点处理
if (
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
const context: ReactContext<mixed> = (newChild: any);
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
readContextDuringReconcilation(returnFiber, context, lanes),
lanes,
);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
// 文本类型处理
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild, // 数字类型最终被转换为了字符串
lanes,
),
);
}
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
这个方法就是根据节点类型来创建新的节点,如果子节点是数组,Fragment context 等,则处理后再次调用自身。如果是"单节点",则调用下面方法返回:
return placeSingleChild(reconcileSingleElement(xxx))
reconcileSingleElement
这个方法是用于处理单节点 diff 的,本文重点是了解 beginWork 阶段的流程,因此不深入 diff,仅以该方法为例。(✨约1477行):
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 更新处理: 单节点 diff:除了可以通过 key 优化的节点都删除
while (child !== null) {
// 存在 key 进行优化
if (child.key === key) {
const elementType = element.type;
// 两个 FRAGMENT
if (elementType === REACT_FRAGMENT_TYPE && child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling); // 右兄弟打上删除标签
// 创建一个新的 Fiber 节点并设置为当前节点的父节点,并返回
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
} else if (
// elementType 相同或者是懒加载类型
child.elementType === elementType ||
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
// 逻辑和上面类似,右兄弟打上删除标签,创建新节点并返回
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
}
deleteRemainingChildren(returnFiber, child);
break;
} else {
// 无法进行 key 优化,暴力删除
deleteChild(returnFiber, child);
}
// 更新为兄弟节点,进入下一次更新
child = child.sibling;
}
// 创建逻辑
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
总的来说分为两个过程:
- 兄弟节点更新判断: 只要不是特殊的 Fragment,懒加载,存在 key 优化情形;节点直接删掉,非常暴力。
- 创建新的节点替换。
createFiberFromElement
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let source = null;
let owner = null;
const type = element.type;
const key = element.key;
const pendingProps = element.props;
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
source,
owner,
mode,
lanes,
);
return fiber;
}
节点的创建在前面讲 FiberNode 的时候仔细分析过了,这里简单看一下。这里创建的节点最终会替换到 workInProgress.child 上,成为构建树中的新节点。然后开始下一个 FiberNode 的替换过程。
最后我们整理一下 beginWork 更新组件的大致流程(单节点 diff 为例):