mutation 阶段
接下来到了执行 DOM 操作的 mutation 阶段的工作
在 before mutation 阶段中,会一上一下的之行 begin 和 complete 的工作,最后 nextEffect 又回到了起始点
mutation 阶段会用同样的方式,向下遍历,向上归并,执行对应的函数,这里执行的是 commitMutationEffects 函数,它会通过调用 commitMutationEffects_begin函数来开始本次的 mutation 阶段的工作
React 将每一个阶段又分为了 begin 和 complete,这样将逻辑进行抽离,主函数流程更加清晰
export function commitMutationEffects(
root: FiberRoot,
firstChild: Fiber,
committedLanes: Lanes,
) {
inProgressLanes = committedLanes; // 优先级相关
inProgressRoot = root;
nextEffect = firstChild;
commitMutationEffects_begin(root);
inProgressLanes = null;
inProgressRoot = null;
}commitMutationEffects_begin
可以看到在这个函数中,主体是一个 while 循环,会从 rootFiber 开始向下遍历,和 before mutation 的工作一样,找到最底层的有 mutation 标志的 fiber 节点,执行 commitMutationEffects_complete 函数
如果遍历到的 Fiber 上有 Deletion 标记,则调用 commitDeletion函数,分离 ref 引用,并调用 componentWillUnmount 生命周期函数,断开 Fiber 与父节点的连接关系。这些工作都在 commitDeletion 函数中进行处理
这是在 React 17.0.3 之后才启用的字段,会在需要被 delete 掉的 Fiber 节点上的 deletions 字段上打上标记,这样可以直接通过 deletions 字段来判断是否需要删除该节点
function commitMutationEffects_begin(root: FiberRoot) {
while (nextEffect !== null) {
const fiber = nextEffect;
const deletions = fiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
// 断开当前 Fiber 节点与 父节点之间的连接
// 分离 ref ,调用 componentWillUnmount
commitDeletion(root, childToDelete, fiber);
} catch (error) {
...
}
}
}
const child = fiber.child;
// ... 省去判断逻辑 nextEffect = child;
commitMutationEffects_complete(root);
}
}commitMutationEffects_complete
在 commitMutationEffects_complete 函数中,会开始归并,优先处理兄弟节点,最后处理父节点,调用 commitMutationEffectsOnFiber 函数,根据不同的组件类型,来执行更新、插入、删除 DOM 的操作
function commitMutationEffects_complete(root: FiberRoot) {
while (nextEffect !== null) {
const fiber = nextEffect;
...
// 核心,根据不同的类型,进行处理
commitMutationEffectsOnFiber(fiber, root);
...
const sibling = fiber.sibling;
if (sibling !== null) {
ensureCorrectReturnPointer(sibling, fiber.return);
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}commitMutationEffectsOnFiber
在 commitMutationEffectsOnFiber 函数中
- 首先会判断是否需要重置文本节点
- 然后判断是否有
ref的更新 - 然后会根据 Fiber 上的
flags的类型进行二进制计算,根据计算结果来执行不同的操作逻辑,这和前面介绍的effectTag的计算是相同的。会有多个 case 存在- Placement:执行
commitPlacement函数插入 DOM 节点,然后删除 Placement 的 effectTag - Update:执行
commitWork函数来执行更新操作,然后删除 Update 的 effectTag - PlacementAndUpdate:先调用
commitPlacement执行插入操作,然后再调用commitWork执行更新操作。
- Placement:执行
对于 Deletion 的操作已经前置处理了,这里不介绍
function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
const flags = finishedWork.flags;
// 判断是否存在 文本节点,重置文本节点
if (flags & ContentReset) {
commitResetTextContent(finishedWork);
}
if (flags & Ref) {
const current = finishedWork.alternate;
if (current !== null) {
commitDetachRef(current);
}
if (enableScopeAPI) {
if (finishedWork.tag === ScopeComponent) {
commitAttachRef(finishedWork);
}
}
}
// ... 处理副作用
const primaryFlags = flags & (Placement | Update | Hydrating);
outer: switch (primaryFlags) {
case Placement: {
commitPlacement(finishedWork);
finishedWork.flags &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(finishedWork);
finishedWork.flags &= ~Placement;
// Update
const current = finishedWork.alternate;
commitWork(current, finishedWork);
break;
}
// SSR 相关 case
...
case Update: {
const current = finishedWork.alternate;
commitWork(current, finishedWork);
break;
}
}
}接下来我们来看看相应的对真实 DOM 节点的操作是如何进行的
Placement 插入节点
当 flags 包含 Placement 的 effectTag 时,会调用这个 commitPlacement 函数来执行对 DOM 节点的插入操作
主要的思路是
- 首先会根据当前的 Fiber 节点,来找到离他最近的 Host 类型的 Parent Fiber 节点
Host 类型包括:HostComponent、HostRoot ...
- 然后根据
parentFiber 节点的tag类型,来判断父 Fiber 节点对应的 DOM 节点是否可以作为container容器,因为父节点有可能是一个component这样就不能直接插入 - 当找到
parent Fiber之后,如果parent Fiber上存在contentReset的effectTag,就需要执行resetTextContent,来重置文本 - 接下来会找到当前
Fiber节点的 Host 类型的slibing节点- 当执行
insertBefore时,就需要知道当前 Fiber 节点对应的兄弟节点 - 当需要执行
appendChild时,需要知道当前 Fiber 节点的 Host 类型 Parent 节点
- 当执行
- 根据是否可以作为
container,来调用不同的函数在指定的位置插入新的节点。实际上这两个函数的处理逻辑是一致的,唯一的区别就是需不需要判断父节点是不是COMMENT_NODE
function commitPlacement(finishedWork: Fiber): void {
// NOTE:如果不支持 mutation 会直接返回了
if (!supportsMutation) {
return;
}
// NOTE:根据当前节点找到离他最近的 host 类型 fiber 节点
// getHostParentFiber 一直向上递归查找,直到找到为止
const parentFiber = getHostParentFiber(finishedWork);
let parent;
let isContainer;
const parentStateNode = parentFiber.stateNode;
//根据父节点的 tag 类型,来判断是否能够作为被插入节点的container,(有可能是组件形式)
switch (parentFiber.tag) {
case HostComponent:
parent = parentStateNode;
isContainer = false;
break;
case HostRoot:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
...
}
// 如果父节点有 ContentReset 的 flags,则重置其文本内容
if (parentFiber.flags & ContentReset) {
resetTextContent(parent);
parentFiber.flags &= ~ContentReset;
}
// 找到 host 的兄弟节点,需要在哪插入
const before = getHostSibling(finishedWork);
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}insertOrAppendPlacementNodeIntoContainer
在这个函数中分为两部分
- 如果是原生 DOM 节点,调用
insertInContainerBefore或appendChildToContainer来在相应的位置插入 DOM 节点 - 如果不是原生 DOM 节点,会对当前 Fiber 节点的所有子 Fiber 节点调用
insertOrAppendPlacementNodeIntoContainer对自身进行遍历,直到找到 DOM 节点,然后插入
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
const {tag} = node;
// 判断当前节点是否为原生的 DOM 节点
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
if (before) {
// 插入
insertInContainerBefore(parent, stateNode, before);
} else {
// 追加
appendChildToContainer(parent, stateNode);
}
} else if (tag === HostPortal) {
// 不处理
} else {
// 不是原生 DOM 节点,需要遍历插入当前节点的子节点
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}insertInContainerBefore 插入节点
当 before 存在时,会进入这个逻辑。也说明当前需要插入节点的前一个节点是明确的了
在这里需要判断当前父节点是否为注释类型的节点
- 如果是注释类型的节点,会在父节点的父节点下插入新的 DOM 节点
- 如果不是,则调用原生 DOM 节点的
insertBefore方法来直接插入节点
export function insertInContainerBefore(
container: Container,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance | SuspenseInstance,
): void {
if (container.nodeType === COMMENT_NODE) {
(container.parentNode: any).insertBefore(child, beforeChild);
} else {
container.insertBefore(child, beforeChild);
}
}appendChildToContainer 差不多,不多讲,源码位置 packages/react-dom/src/client/ReactDOMHostConfig.js
Update 更新节点
在前面的代码中我们也能看到,在更新节点时,都会调用 commitWork函数来处理
下面我们来揭开它的面纱
commitWork
commitWork 函数会对不同类型的更新做出处理,重点关注 HostComponent 和 HostText 类型
整体流程如下
- 首先会判断是否支持 mutation,执行其他的逻辑,这里我们的宿主环境不会进入当前逻辑,跳过这部分
- 接下来会根据 Fiber 节点的 tag 类型,进入不同的条件语句:
对于和 Function Component 相关的类型,例如 simpleMemoComponent、functionComponent 等类型,会执行 commitHookEffectListUnmount函数,也就是会调用 useLayoutEffect 或 useInsertionEffect 的销毁函数
具体是会遍历当前的 updateQueue 链表,如果当前 Fiber 节点的 effectTag 等于传入的 tag(HookLayout | Insertion),这个 effectTag 就表示,当前 Fiber 节点包含对 useLayoutEffect 或 useInsertionEffect 的调用,会执行它们的销毁函数
function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
...
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
...
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}- 对于 HostComponent 类型的节点,首先会获取到 新旧props以及
updateQueue,最后调用commitUpdate来对 DOM 进行更新
case HostComponent: {
// 获取对应的 DOM 节点
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// 新旧 props
const newProps = finishedWork.memoizedProps;
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// 获取 updateQueue
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null; // 清空
if (updatePayload !== null) {
// 提交更新
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}- 对于 HostText 类型的更新,首先获取到真实的文本节点、新旧文本的内容,调用
commitTextUpdate来更新文本节点的 nodeValue
// FC 相关的 case 调用 commitHookEffectListUnmount
case HostText: {
// ...错误处理
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
const oldText: string =
current !== null ? current.memoizedProps : newText;
// 更新新旧 text
commitTextUpdate(textInstance, oldText, newText);
return;
}
// ... 不关注
}commitUpdate
在 commitWork 中,会调用 commitUpdate 函数来进行元素的更新,commitUpdate 主要做以下几件事
- 执行
domElement[internalPropsKey] = props,来更新 props - 然后调用
updateProperties函数,来更新 DOM 的属性,将diff的结果应用到真实 DOM 上,首先会对 radio 进行特殊的处理,然后会调用updateDOMProperties,然后根据 Fiber 的 tag 类型,对 input、textarea、select 等表单类型的节点做处理
export function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
// domElement[internalPropsKey] = props
updateFiberProps(domElement, newProps);
// 将 diff 结果应用于真实DOM
updateProperties(domElement, updatePayload, type, oldProps, newProps);
}updateDOMProperties
在 updateDOMProperties 中会遍历 updateQueue 链表,将更新作用到真实 DOM 节点上,根据 propKey 进行不同的更新操作
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean,
): void {
// 遍历 updatePayload
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === STYLE) {
// 处理 style
setValueForStyles(domElement, propValue);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
// 处理 innerHtml
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
// 处理 children
setTextContent(domElement, propValue);
} else {
// 处理其他节点属性
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}接下来执行 layout 阶段