Skip to content

commitMutationEffects

Commit阶段对于副作用的第二次遍历处理过程。

在这个阶段主要就是将DOM元素更新到浏览器中。

其主要作用如下:

  1. 将DOM元素更新到浏览器中
  2. ref.current 的解绑
  3. useLayoutEffect 销毁函数的回调
  4. 原生标签元素通过 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

  1. 为什么不是 div

因为div其也是一个 插入类型 的副作用FiberNode,所以第一次 p的兄弟节点为 div2 , 然后通过inertBefore(p , div#2)进行插入,然后再进行div的插入。

  1. 对于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;
    }
  }
}

其重点在:

  1. FunctionComponent的useLayoutEffect钩子的destroy的执行
  2. 原生组件类型的 updateQueue 结构更新DOM元素

删除类型(Deletion)

主要流程为

  1. 递归调用Fiber节点及其子孙Fiber节点fiber.tagClassComponentcomponentWillUnmount生命周期钩子,从页面移除Fiber节点对应DOM节点
  2. 解绑ref
  3. 调度useEffect的销毁函数

栗子

image

结果为:

image

源码解析

  1. 组件卸载的顺序是什么?

其卸载顺序是:

  • 从上至下执行组件的卸载流程
  • 子孙节点优先,然后是兄弟节点的卸载
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;
  }
}
  1. 卸载涉及到那些函数或者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;
    }