Appearance
commitMutationEffects
Commit阶段对于副作用的第二次遍历处理过程。
在这个阶段主要就是将DOM元素更新到浏览器中。
其主要作用如下:
- 将DOM元素更新到浏览器中
- ref.current 的解绑
- useLayoutEffect 销毁函数的回调
- 原生标签元素通过 updateQueue 进行属性的更新
将DOM元素更新到浏览器中
文本重置类型
js
// 文本内容重置
if (flags & ContentReset) {
commitResetTextContent(nextEffect);
}
比较简单,就是 textContext = ""
插入类型(Placement)
commitPlacement(nextEffect)
注意的是对于兄弟DOM节点的查找的过程。
js
/**
* 插入类型的副作用处理
* @param {*} finishedWork
* @returns
*/
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
// Recursively insert all host nodes into the parent.
// 找到祖先中第一个 原生组件类型 的 Fiber
// div#1 -> App -> div#2 那么div#2的parentFiber就是 div#1 而不是 App
const parentFiber = getHostParentFiber(finishedWork);
// Note: these two variables *must* always be updated together.
// 获取 父Fiber对应的DOM元素
let parent;
// 获取 父Fiber 是否可以使用 insert
let isContainer;
const parentStateNode = parentFiber.stateNode;
switch (parentFiber.tag) {
case HostComponent:
parent = parentStateNode;
isContainer = false;
break;
case HostRoot:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
case HostPortal:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
case FundamentalComponent:
if (enableFundamentalAPI) {
parent = parentStateNode.instance;
isContainer = false;
}
// eslint-disable-next-line-no-fallthrough
default:
invariant(
false,
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
if (parentFiber.flags & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.flags &= ~ContentReset;
}
// 获取DOM结构上真正的DOM元素兄弟节点
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}
/**
* append或者 insertBefore 插入到容器节点中
* @param {*} node
* @param {*} before
* @param {*} parent
*/
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
const {tag} = node;
// 是否是原生组件类型的节点
const isHost = tag === HostComponent || tag === HostText;
if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) {
// 原生组件类型 直接append或者inertBefore
const stateNode = isHost ? node.stateNode : node.stateNode.instance;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
} else if (tag === HostPortal) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else {
// 其他的 插入子FiberNode
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;
}
}
}
}
getHostParentFiber(fiber: Fiber)
getHostSibling(fiber: Fiber)
获取第一个 原生组件类型 的兄弟节点。
这是一个非常复杂的操作。因为React的FiberNode树结构与真正的DOM树结构不一定是一一对应的,FiberNode树中存在需要虚拟的节点(HostPortal、ClassComponent、FunctionComponent),其没有真正的DOM元素结构,那么我们FiberNode.sibling的节点在DOM树上就不一定是其兄弟节点
js
HostRoot -> App -> div#1 -> Child -> div#2
// 真正的DOM结构是
div#root -> div#1 -> div#2
在这种情况下寻找其兄弟节点变得特别的复杂。
如
js
const Child = props => <div>{ props.count }</div>;
const App = props => {
return <div>
{
visible && (
<p>1</p>
<Child count={count} />
)
}
<div id="2"></div>
</div>
}
当我们visible变成true的时候 p的兄弟节点是什么?
js
FiberNode
div -> p
Child -> div
div#2
// 真实的DOM结构
div -> p
div
div#2
答案是: div#2
- 为什么不是 div
因为div其也是一个 插入类型 的副作用FiberNode,所以第一次 p的兄弟节点为 div2 , 然后通过inertBefore(p , div#2)
进行插入,然后再进行div的插入。
- 对于div的插入,其应该是 appendChild ,实际上却是
inertBefore(div , div#2)
同样还是上面的问题,FiberNode的树与DOM树结构不一定相同的,Child其只是一个虚拟的DOM节点。所以其走的是while (node.sibling === null) {}
这个过程
我们先简单看一下代码
js
/**
* 获取原生组件类型的兄弟组件(指数级的操作)
* @param {*} fiber
* @returns
*/
function getHostSibling(fiber: Fiber): ?Instance {
// We're going to search forward into the tree until we find a sibling host
// node. Unfortunately, if multiple insertions are done in a row we have to
// search past them. This leads to exponential search for the next sibling.
// TODO: Find a more efficient way to do this.
let node: Fiber = fiber;
siblings: while (true) {
// If we didn't find anything, let's try the next sibling.
// 如果兄弟节点为null,那就去寻找其父节点的兄弟节点(判断是否是原生组件类型)
// 对应下面这个Child插入的这个栗子
while (node.sibling === null) {
if (node.return === null || isHostParent(node.return)) {
// If we pop out of the root or hit the parent the fiber we are the
// last sibling.
return null;
}
node = node.return;
}
/*
// 将当前父节点作为兄弟节点的父节点
// 处理 这种情况,div本身没有兄弟节点,但是其在DOM结构上的兄弟节点为 div#2
// 所以当node.sibling == null 的时候 走到上面的while循环,找到父元素的兄弟节点
const Child = props => <div>{ props.count }</div>;
const App = props => {
return <div>
{
visible && (
<Child count={count} />
)
}
<div id="2"></div>
</div>
}
*/
node.sibling.return = node.return;
// 赋值兄弟
node = node.sibling;
// 如果不是原生类型组件(不具备具体的DOM),需要特殊处理
while (
node.tag !== HostComponent &&
node.tag !== HostText &&
node.tag !== DehydratedFragment
) {
// If it is not host node and, we might have a host node inside it.
// Try to search down until we find one.
// 如果其兄弟节点不是原生类型组件且有插入的副作用 , 继续向下一个兄弟节点
// 这个兄弟节点会在下一个中处理
if (node.flags & Placement) {
// If we don't have a child, try the siblings instead.
continue siblings;
}
// If we don't have a child, try the siblings instead.
// We also skip portals because they are not part of this host tree.
// 如果没有子节点 或者是 HostPortal 类型的
// 对于这种 不会生成DOM元素,继续向下
if (node.child === null || node.tag === HostPortal) {
continue siblings;
} else {
// 如果有子节点,那就得向子节点遍历了
node.child.return = node;
node = node.child;
}
}
// Check if this host node is stable or about to be placed.
if (!(node.flags & Placement)) {
// Found it!
return node.stateNode;
}
}
}
更新类型(Update)
js
/**
* 更新DOM
* @param {*} current
* @param {*} finishedWork
* @returns
*/
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
if (!supportsMutation) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
// 执行 useLayoutEffect 的 destroy 的 effect
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
}
return;
}
case Profiler: {
return;
}
case SuspenseComponent: {
commitSuspenseComponent(finishedWork);
attachSuspenseRetryListeners(finishedWork);
return;
}
case SuspenseListComponent: {
attachSuspenseRetryListeners(finishedWork);
return;
}
case HostRoot: {
if (supportsHydration) {
const root: FiberRoot = finishedWork.stateNode;
if (root.hydrate) {
// We've just hydrated. No need to hydrate again.
root.hydrate = false;
commitHydratedContainer(root.containerInfo);
}
}
break;
}
case OffscreenComponent:
case LegacyHiddenComponent: {
return;
}
}
commitContainer(finishedWork);
return;
}
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// 执行 useLayoutEffect 的 destroy 的 effect
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
return;
}
case ClassComponent: {
return;
}
// 原生标签组件
case HostComponent: {
// DOM元素实例对象
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
// 获取组件上更新的队列 ["style",{color: 'green'} ]
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}
// 文本类型
case HostText: {
invariant(
finishedWork.stateNode !== null,
'This should have a text node initialized. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
// 文本的 DOM元素 对象
const textInstance: TextInstance = finishedWork.stateNode;
// 新的文本
const newText: string = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
// 历史文本
const oldText: string =
current !== null ? current.memoizedProps : newText;
// 进行文本DOM元素的更新操作
commitTextUpdate(textInstance, oldText, newText);
return;
}
case HostRoot: {
if (supportsHydration) {
const root: FiberRoot = finishedWork.stateNode;
if (root.hydrate) {
// We've just hydrated. No need to hydrate again.
root.hydrate = false;
commitHydratedContainer(root.containerInfo);
}
}
return;
}
case Profiler: {
return;
}
case SuspenseComponent: {
commitSuspenseComponent(finishedWork);
attachSuspenseRetryListeners(finishedWork);
return;
}
case SuspenseListComponent: {
attachSuspenseRetryListeners(finishedWork);
return;
}
case IncompleteClassComponent: {
return;
}
case FundamentalComponent: {
if (enableFundamentalAPI) {
const fundamentalInstance = finishedWork.stateNode;
updateFundamentalComponent(fundamentalInstance);
return;
}
break;
}
case ScopeComponent: {
if (enableScopeAPI) {
const scopeInstance = finishedWork.stateNode;
prepareScopeUpdate(scopeInstance, finishedWork);
return;
}
break;
}
case OffscreenComponent:
case LegacyHiddenComponent: {
const newState: OffscreenState | null = finishedWork.memoizedState;
const isHidden = newState !== null;
hideOrUnhideAllChildren(finishedWork, isHidden);
return;
}
}
}
其重点在:
- FunctionComponent的useLayoutEffect钩子的destroy的执行
- 原生组件类型的 updateQueue 结构更新DOM元素
删除类型(Deletion
)
主要流程为
- 递归调用
Fiber节点
及其子孙Fiber节点
中fiber.tag
为ClassComponent
的componentWillUnmount生命周期钩子,从页面移除Fiber节点
对应DOM节点
- 解绑
ref
- 调度
useEffect
的销毁函数
栗子
结果为:
源码解析
- 组件卸载的顺序是什么?
其卸载顺序是:
- 从上至下执行组件的卸载流程
- 子孙节点优先,然后是兄弟节点的卸载
txt
// Child -> Child1 -> Child11 -> Child111
// -> Child112
// -> Child12 -> Child121
// -> Child2 -> Child21
// 其卸载过程是
// Child -> Child1 -> Child11 -> Child111 -> Child112 -> Child12 -> Child121 -> Child2 -> Child21
js
/**
* 组件卸载的执行函数
* 卸载流程:
* 1. 从上至下执行组件的卸载流程
* 2. 子孙节点优先,然后是兄弟节点的卸载
*
* Child -> Child1 -> Child11 -> Child111
* -> Child112
* -> Child12 -> Child121
* -> Child2 -> Child21
* 其卸载过程是 Child -> Child1 -> Child11 -> Child111 -> Child112 -> Child12 -> Child121 -> Child2 -> Child21
*/
function commitNestedUnmounts(
finishedRoot: FiberRoot,
root: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
let node: Fiber = root;
while (true) {
// 节点本身的卸载工作
commitUnmount(finishedRoot, node, renderPriorityLevel);
// Visit children because they may contain more composite or host nodes.
// Skip portals because commitUnmount() currently visits them recursively.
// 深度优先遍历的 优先执行节点子孙组件的卸载
if (
node.child !== null &&
// If we use mutation we drill down into portals using commitUnmount above.
// If we don't use mutation we drill down into portals here instead.
(!supportsMutation || node.tag !== HostPortal)
) {
node.child.return = node;
node = node.child;
continue;
}
if (node === root) {
return;
}
// 在执行节点兄弟的卸载工作
while (node.sibling === null) {
if (node.return === null || node.return === root) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}
- 卸载涉及到那些函数或者Hooks
真正执行卸载的是 commitUnmount(finishedRoot, node, renderPriorityLevel)
方法,此方法也是通过判断FiberNode的类型执行不同的处理方法,下面我们就看一下这个方法(主要是 函数式组件 和 Class组件)
函数式组件
- useEffect 类型副作用的destroy加入到scheduleCallback 任务队列进行回调
- useLayoutEffect类型的,直接回调 destroy函数
所以才有上图中 先执行xxx useLayoutEffect destroy
,然后执行xx useEffect destroy
js
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// 函数式组件
const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
if (updateQueue !== null) {
// 设计的还是 firstEffect lastEffect 副作用队列
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const {destroy, tag} = effect;
// 只有 useLayoutEffect useEffect的副作用才会存在 destroy
if (destroy !== undefined) {
/*
如果是 useEffect 类型的,加入到 scheduleCallback 任务队列进行回调
如果是其他的如useLayoutEffect类型的,直接回调 destroy函数
*/
if ((tag & HookPassive) !== NoHookEffect) {
enqueuePendingPassiveHookEffectUnmount(current, effect);
} else {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
current.mode & ProfileMode
) {
startLayoutEffectTimer();
safelyCallDestroy(current, destroy);
recordLayoutEffectDuration(current);
} else {
safelyCallDestroy(current, destroy);
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
return;
}
Class组件
- 解绑 ref
- 执行Class组件的componentWillUnmount周期函数
js
// Class 组件类型
case ClassComponent: {
// 解绑 ref
safelyDetachRef(current);
const instance = current.stateNode;
// 执行Class组件的componentWillUnmount周期函数
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(current, instance);
}
return;
}