快来看 React 渲染过程(下)

149 阅读5分钟

引言

书接上回,上一篇文章主要是介绍了 React 初次渲染过程中在内存中生成 workInProgress树的过程。主要是经历了两个阶段,一个是 beginWork,另一个是 completeWork。那React 是如何将已经生成的 WorkInProgress 树渲染到宿主环境中的呢?接下来说一说 commit 阶段,也就是将 workInProgress 树渲染到页面上的过程。

原理

demo

因为 commit 阶段涉及到副作用的执行,比如 useLayoutEffect 等。 所以我们改造一下调试源码用的组件代码。

import React, { useEffect, useState } from "react"
const Index = ()=>{
    debugger;
    const [state,setState] = useState(0)
    useEffect(()=>{
        console.log(123)
    },[])
    console.log('渲染吗??')
    const [demo,setDemo] = useState('demo')

    return <div>
        {state}
        <button onClick={()=>{
            // debugger
            // setState(1)
            // setState(10)
            // setState(100)
            setState(0)
        }}>触发渲染</button>    
    </div>
}
export default Index

我们的组件大概是这个样子的。下面我们来看一下 React 整个过程是怎么样的。

过程

首先看一下当走完beginWork 和completeWork 之后,生成的树是怎么样的。

image.png 可以看到,最后是生成了一个这样的树,我们的 wip 树保存在finishedWork 中,可以打印出来看一下

image.png 进入 commit 阶段的入口函数是 commitRoot

function commitRoot(root) {

  // TODO: This no longer makes any sense. We already wrap the mutation and
  // layout phases. Should be able to remove.
  var previousUpdateLanePriority = getCurrentUpdatePriority();
  var prevTransition = ReactCurrentBatchConfig$3.transition;

  try {
    ReactCurrentBatchConfig$3.transition = 0;
    setCurrentUpdatePriority(DiscreteEventPriority);
    commitRootImpl(root, previousUpdateLanePriority);
  } finally {
    ReactCurrentBatchConfig$3.transition = prevTransition;
    setCurrentUpdatePriority(previousUpdateLanePriority);
  }

  return null;
}

下面从这个函数出发具体来看看 React 做了什么

commitRoot

commitRoot 主要的逻辑就是调用了一个commitRootImpl,同时传入了 root 树和previousUpdateLanePriority。

commitRootImpl

commitRootImpl 的作用就大了许多,我们分别来看commitRootImpl 做了哪些事情。

function commitRootImpl(root, renderPriorityLevel) {
  do {
    // 清空副作用列表
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);

  flushRenderPhaseStrictModeWarningsInDEV();

  
    //  判断当前 fiber 节点的子孙节点是否有副作用
  var subtreeHasEffects = (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;
  //  判断当前节点是否有副作用
  var rootHasEffect = (finishedWork.flags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;

  if (subtreeHasEffects || rootHasEffect) {
   

    var shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(root, finishedWork);


    commitMutationEffects(root, finishedWork, lanes);

   

    commitLayoutEffects(finishedWork, root, lanes);

    {
      markLayoutEffectsStopped();
    }
    // opportunity to paint.


    requestPaint();
  
  return null;
}

从源码可以看到,commitRootImpl 主要做了三件事情

  1. 通过一个while 循环去调用flushPassiveEffects去清除副作用,防止因为有没有运行的副作用影响 commit过程。
  2. 判断子孙节点以及自己是否有副作用
    • 如果有副作用,依次执行commitBeforeMutationEffects,commitMutationEffects,commitLayoutEffects,requestPaint
    • 如果没有副作用,直接切树
  3. 其他一些收尾工作。

下面我们依次进行说明

判断副作用

在completeWork 阶段,React 将子组件的 flags 都冒泡到了父组件的subtreeFlags 中,所以可以通过subtreeFlags来检查子组件是否有副作用已经自己本身是否有副作用。

  var subtreeHasEffects = (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;
  var rootHasEffect = (finishedWork.flags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;

如果有副作用的话,就依次运行commitBeforeMutationEffects ,commitMutationEffects,commitLayoutEffects,requestPaint。下面分别进行说明。 如果友友们对位运算不太熟悉,可以自行百度了解一下~

commitBeforeMutationEffects

function commitBeforeMutationEffects(root, firstChild) {
  focusedInstanceHandle = prepareForCommit(root.containerInfo);
  nextEffect = firstChild;
  commitBeforeMutationEffects_begin(); // We no longer need to track the active instance fiber

  var shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;
  return shouldFire;
}

主要流程是commitBeforeMutationEffects_begin处理的,我们直接进入commitBeforeMutationEffects_begin查看 React 做了什么。

commitBeforeMutationEffects_begin
function commitBeforeMutationEffects_begin() {
  while (nextEffect !== null) {
    var fiber = nextEffect; // This phase is only used for beforeActiveInstanceBlur.

    var child = fiber.child;

    if ((fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && child !== null) {
      ensureCorrectReturnPointer(child, fiber);
      nextEffect = child;
    } else {
      commitBeforeMutationEffects_complete();
    }
  }
}

主要逻辑在commitBeforeMutationEffects_complete中

commitBeforeMutationEffects_complete
function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    var fiber = nextEffect;
    setCurrentFiber(fiber);

    try {
     
      commitBeforeMutationEffectsOnFiber(fiber);
    } catch (error) {
      reportUncaughtErrorInDEV(error);
      captureCommitPhaseError(fiber, fiber.return, error);
    }

    resetCurrentFiber();
    var sibling = fiber.sibling;

    if (sibling !== null) {
      ensureCorrectReturnPointer(sibling, fiber.return);
      nextEffect = sibling;
      return;
    }

    nextEffect = fiber.return;
  }
}

这里解释一下,这个循环的作用主要是运行了commitBeforeMutationEffectsOnFiber,同时如果当前 fiber 节点有sibling节点的话,也会运行commitBeforeMutationEffects_complete。整个逻辑是一个从下到上的过程。

commitBeforeMutationEffectsOnFiber
function commitBeforeMutationEffectsOnFiber(finishedWork) {
  var current = finishedWork.alternate;
  var flags = finishedWork.flags;

  if ((flags & Snapshot) !== NoFlags) {
    setCurrentFiber(finishedWork);

    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent:
        {
          break;
        }

      case ClassComponent:
        {
          if (current !== null) {
            var prevProps = current.memoizedProps;
            var prevState = current.memoizedState;
            var instance = finishedWork.stateNode; // We could update instance props and state here,
            // but instead we rely on them being set during last render.
            // TODO: revisit this when we implement resuming.

            {
              if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {
                if (instance.props !== finishedWork.memoizedProps) {
                  error('Expected %s props to match memoized props before ' + 'getSnapshotBeforeUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                }

                if (instance.state !== finishedWork.memoizedState) {
                  error('Expected %s state to match memoized state before ' + 'getSnapshotBeforeUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.state`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                }
              }
            }

            var snapshot = instance.getSnapshotBeforeUpdate(finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState);

            {
              var didWarnSet = didWarnAboutUndefinedSnapshotBeforeUpdate;

              if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) {
                didWarnSet.add(finishedWork.type);

                error('%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + 'must be returned. You have returned undefined.', getComponentNameFromFiber(finishedWork));
              }
            }

            instance.__reactInternalSnapshotBeforeUpdate = snapshot;
          }

          break;
        }

      case HostRoot:
        {
          {
            var root = finishedWork.stateNode;
            clearContainer(root.containerInfo);
          }

          break;
        }

      case HostComponent:
      case HostText:
      case HostPortal:
      case IncompleteClassComponent:
        // Nothing to do for these component types
        break;

      default:
        {
          throw new Error('This unit of work tag should not have side-effects. This error is ' + 'likely caused by a bug in React. Please file an issue.');
        }
    }

    resetCurrentFiber();
  }
}

这里判断当前 fiber 是否有Snapshot副作用,如果有副作用的话,根据 tag 进入不同的 case,主要有作用的 case 就两个,其余都是凑数的,没什么实际用途。

      case ClassComponent:
        {
          if (current !== null) {
            var prevProps = current.memoizedProps;
            var prevState = current.memoizedState;
            var instance = finishedWork.stateNode; // We could update instance props and state here,
            // but instead we rely on them being set during last render.
            // TODO: revisit this when we implement resuming.

            {
              if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {
                if (instance.props !== finishedWork.memoizedProps) {
                  error('Expected %s props to match memoized props before ' + 'getSnapshotBeforeUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                }

                if (instance.state !== finishedWork.memoizedState) {
                  error('Expected %s state to match memoized state before ' + 'getSnapshotBeforeUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.state`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                }
              }
            }

            var snapshot = instance.getSnapshotBeforeUpdate(finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState);

            {
              var didWarnSet = didWarnAboutUndefinedSnapshotBeforeUpdate;

              if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) {
                didWarnSet.add(finishedWork.type);

                error('%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + 'must be returned. You have returned undefined.', getComponentNameFromFiber(finishedWork));
              }
            }

            instance.__reactInternalSnapshotBeforeUpdate = snapshot;
          }

          break;
        }

      case HostRoot:
        {
          {
            var root = finishedWork.stateNode;
            clearContainer(root.containerInfo);
          }

          break;
        }

如果是一个类组件的话,会执行getSnapshotBeforeUpdate这个函数来做一些事情。因为对类组件了解不够深,这里跳过说明。 如果是HostRoot,会清除节点内容。 具体可看下面代码

function clearContainer(container) {
  if (container.nodeType === ELEMENT_NODE) {
    container.textContent = '';
  } else if (container.nodeType === DOCUMENT_NODE) {
    var body = container.body;

    if (body != null) {
      body.textContent = '';
    }
  }
} 

这里是为了防止本身 html 节点中有内容,影响我们 React 的整个渲染。 下面我们用图来看一下整个执行过程

image.png

commitMutationEffects

和上面的流程差不多,看代码

function commitMutationEffects(root, firstChild, committedLanes) {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  nextEffect = firstChild;
  commitMutationEffects_begin(root);
  inProgressLanes = null;
  inProgressRoot = null;
}

依旧是commitMutationEffects_begin来进行一些逻辑的处理。

commitMutationEffects_begin
function commitMutationEffects_begin(root) {
  while (nextEffect !== null) {
    var fiber = nextEffect; // TODO: Should wrap this in flags check, too, as optimization

    var deletions = fiber.deletions;

    if (deletions !== null) {
      for (var i = 0; i < deletions.length; i++) {
        var childToDelete = deletions[i];

        try {
          commitDeletion(root, childToDelete, fiber);
        } catch (error) {
          reportUncaughtErrorInDEV(error);
          captureCommitPhaseError(childToDelete, fiber, error);
        }
      }
    }

    var child = fiber.child;

    if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) {
      ensureCorrectReturnPointer(child, fiber);
      nextEffect = child;
    } else {
      commitMutationEffects_complete(root);
    }
  }
}

该函数主要做了两件事:

  1. 如果存在deletions ,先处理这部分的逻辑。
  2. 然后进入commitMutationEffects_complete。
commitMutationEffects_complete
function commitMutationEffects_complete(root) {
  while (nextEffect !== null) {
    var fiber = nextEffect;
    setCurrentFiber(fiber);
    try {
      commitMutationEffectsOnFiber(fiber, root);
    } catch (error) {
      reportUncaughtErrorInDEV(error);
      captureCommitPhaseError(fiber, fiber.return, error);
    }
    resetCurrentFiber();
    var sibling = fiber.sibling;
    if (sibling !== null) {
      ensureCorrectReturnPointer(sibling, fiber.return);
      nextEffect = sibling;
      return;
    }

    nextEffect = fiber.return;
  }
}

依旧是熟悉的套路,先执行commitMutationEffectsOnFiber,然后再去判断当前节点是否有sibling,如果有sibling,继续执行commitMutationEffectsOnFiber。

commitMutationEffectsOnFiber
function commitMutationEffectsOnFiber(finishedWork, root) {
  var primaryFlags = flags & (Placement | Update | Hydrating);

   switch (primaryFlags) {
    case Placement:
      {
        commitPlacement(finishedWork); // Clear the "placement" from effect tag so that 
        finishedWork.flags &= ~Placement;
        break;
      }

    case PlacementAndUpdate:
      {
        commitPlacement(finishedWork); // Clear the "placement" from effect tag so that 
        finishedWork.flags &= ~Placement; // Update
        var _current3 = finishedWork.alternate;
        commitWork(_current3, finishedWork);
        break;
      }

    case Hydrating:
      {
        finishedWork.flags &= ~Hydrating;
        break;
      }

    case HydratingAndUpdate:
      {
        finishedWork.flags &= ~Hydrating; // Update

        var _current4 = finishedWork.alternate;
        commitWork(_current4, finishedWork);
        break;
      }

    case Update:
      {
        var _current5 = finishedWork.alternate;
        /*KaSong*/logHook('updateDOM', finishedWork, 'commitWork')
        commitWork(_current5, finishedWork);
        break;
      }
  }
}

主要是判断当前 flags,通过 flags 进入不同的 case 进行处理。初次渲染的话,进入的是Placement分支,主要是执行了以下方法:commitPlacement

function commitPlacement(finishedWork) {


  var parentFiber = getHostParentFiber(finishedWork); // Note: these two variables *must* always be updated together.

  var parent;
  var isContainer;
  var 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;
    // eslint-disable-next-line-no-fallthrough

    default:
      throw new Error('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;
  }

  var 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);
  }
}

通过找到当前节点的父节点parentFiber,执行插入方法insertOrAppendPlacementNodeIntoContainer。

insertOrAppendPlacementNodeIntoContainer
function insertOrAppendPlacementNodeIntoContainer(node, before, parent) {
  var tag = node.tag;
  var isHost = tag === HostComponent || tag === HostText;

  if (isHost) {
    var stateNode = node.stateNode;

    if (before) {
      insertInContainerBefore(parent, stateNode, before);
    } else {
      appendChildToContainer(parent, stateNode);
    }
  } else if (tag === HostPortal) ; else {
    var child = node.child;

    if (child !== null) {
      insertOrAppendPlacementNodeIntoContainer(child, before, parent);
      var sibling = child.sibling;

      while (sibling !== null) {
        insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
}

这个方法就是一个递归的过程,将真实的元素插入到指定的位置。这里有一个判断,只有节点的 tag 为 HostComponent 或者 tag 为 HostText,才代表是可以插入的,执行插入逻辑。

commitLayoutEffects

function commitLayoutEffects(finishedWork, root, committedLanes) {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  nextEffect = finishedWork;
  commitLayoutEffects_begin(finishedWork, root, committedLanes);
  inProgressLanes = null;
  inProgressRoot = null;
}

还是一样的套路,具体的逻辑通过commitLayoutEffects_begin进行执行。

commitLayoutEffects_begin
function commitLayoutEffects_begin(subtreeRoot, root, committedLanes) {
  // Suspense layout effects semantics don't change for legacy roots.
  var isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;

  while (nextEffect !== null) {
    var fiber = nextEffect;
    var firstChild = fiber.child;

    

    if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
      ensureCorrectReturnPointer(firstChild, fiber);
      nextEffect = firstChild;
    } else {
      commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
    }
  }
}

还是一样的套路,开启一个循环,从上到下遍历,同时执行commitLayoutMountEffects_complete

commitLayoutMountEffects_complete
function commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes) {
  while (nextEffect !== null) {
    var fiber = nextEffect;

    if ((fiber.flags & LayoutMask) !== NoFlags) {
      var current = fiber.alternate;
      setCurrentFiber(fiber);

      try {
        commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
      } catch (error) {
        reportUncaughtErrorInDEV(error);
        captureCommitPhaseError(fiber, fiber.return, error);
      }

      resetCurrentFiber();
    }

    if (fiber === subtreeRoot) {
      nextEffect = null;
      return;
    }

    var sibling = fiber.sibling;

    if (sibling !== null) {
      ensureCorrectReturnPointer(sibling, fiber.return);
      nextEffect = sibling;
      return;
    }

    nextEffect = fiber.return;
  }
}

一样的套路,开启了一个循环,去执行commitLayoutEffectOnFiber以及判断当前节点是否有sibling,如果有sibling,继续执行commitLayoutEffectOnFiber。

commitLayoutEffectOnFiber
function commitLayoutEffectOnFiber(finishedRoot, current, finishedWork, committedLanes) {
  if ((finishedWork.flags & LayoutMask) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent:
        {
          if ( !offscreenSubtreeWasHidden) {
            // At this point layout effects have already been destroyed (during mutation phase).
            // This is done to prevent 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 ( finishedWork.mode & ProfileMode) {
              try {
                startLayoutEffectTimer();
                commitHookEffectListMount(Layout | HasEffect, finishedWork);
              } finally {
                recordLayoutEffectDuration(finishedWork);
              }
            } else {
              commitHookEffectListMount(Layout | HasEffect, finishedWork);
            }
          }

          break;
        }

      case ClassComponent:
        {
          var instance = finishedWork.stateNode;

          if (finishedWork.flags & Update) {
            if (!offscreenSubtreeWasHidden) {
              if (current === null) {
                // We could update instance props and state here,
                // but instead we rely on them being set during last render.
                // TODO: revisit this when we implement resuming.
                {
                  if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {
                    if (instance.props !== finishedWork.memoizedProps) {
                      error('Expected %s props to match memoized props before ' + 'componentDidMount. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                    }

                    if (instance.state !== finishedWork.memoizedState) {
                      error('Expected %s state to match memoized state before ' + 'componentDidMount. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.state`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                    }
                  }
                }

                if ( finishedWork.mode & ProfileMode) {
                  try {
                    startLayoutEffectTimer();
                    instance.componentDidMount();
                  } finally {
                    recordLayoutEffectDuration(finishedWork);
                  }
                } else {
                  instance.componentDidMount();
                }
              } else {
                var prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps : resolveDefaultProps(finishedWork.type, current.memoizedProps);
                var prevState = current.memoizedState; // We could update instance props and state here,
                // but instead we rely on them being set during last render.
                // TODO: revisit this when we implement resuming.

                {
                  if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {
                    if (instance.props !== finishedWork.memoizedProps) {
                      error('Expected %s props to match memoized props before ' + 'componentDidUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                    }

                    if (instance.state !== finishedWork.memoizedState) {
                      error('Expected %s state to match memoized state before ' + 'componentDidUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.state`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                    }
                  }
                }

                if ( finishedWork.mode & ProfileMode) {
                  try {
                    startLayoutEffectTimer();
                    instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate);
                  } finally {
                    recordLayoutEffectDuration(finishedWork);
                  }
                } else {
                  instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate);
                }
              }
            }
          } // TODO: I think this is now always non-null by the time it reaches the
          // commit phase. Consider removing the type check.


          var updateQueue = finishedWork.updateQueue;

          if (updateQueue !== null) {
            {
              if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {
                if (instance.props !== finishedWork.memoizedProps) {
                  error('Expected %s props to match memoized props before ' + 'processing the update queue. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                }

                if (instance.state !== finishedWork.memoizedState) {
                  error('Expected %s state to match memoized state before ' + 'processing the update queue. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.state`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
                }
              }
            } // We could update instance props and state here,
            // but instead we rely on them being set during last render.
            // TODO: revisit this when we implement resuming.


            commitUpdateQueue(finishedWork, updateQueue, instance);
          }

          break;
        }

      case HostRoot:
        {
          // TODO: I think this is now always non-null by the time it reaches the
          // commit phase. Consider removing the type check.
          var _updateQueue = finishedWork.updateQueue;

          if (_updateQueue !== null) {
            var _instance = null;

            if (finishedWork.child !== null) {
              switch (finishedWork.child.tag) {
                case HostComponent:
                  _instance = getPublicInstance(finishedWork.child.stateNode);
                  break;

                case ClassComponent:
                  _instance = finishedWork.child.stateNode;
                  break;
              }
            }

            commitUpdateQueue(finishedWork, _updateQueue, _instance);
          }

          break;
        }

      case HostComponent:
        {
          var _instance2 = finishedWork.stateNode; // Renderers may schedule work to be done after host components are mounted
          // (eg DOM renderer may schedule auto-focus for inputs and form controls).
          // These effects should only be committed when components are first mounted,
          // aka when there is no current/alternate.

          if (current === null && finishedWork.flags & Update) {
            var type = finishedWork.type;
            var props = finishedWork.memoizedProps;
            commitMount(_instance2, type, props);
          }

          break;
        }

      case HostText:
        {
          // We have no life-cycles associated with text.
          break;
        }

      case HostPortal:
        {
          // We have no life-cycles associated with portals.
          break;
        }

      case Profiler:
        {
          {
            var _finishedWork$memoize2 = finishedWork.memoizedProps,
                onCommit = _finishedWork$memoize2.onCommit,
                onRender = _finishedWork$memoize2.onRender;
            var effectDuration = finishedWork.stateNode.effectDuration;
            var commitTime = getCommitTime();
            var phase = current === null ? 'mount' : 'update';

            {
              if (isCurrentUpdateNested()) {
                phase = 'nested-update';
              }
            }

            if (typeof onRender === 'function') {
              onRender(finishedWork.memoizedProps.id, phase, finishedWork.actualDuration, finishedWork.treeBaseDuration, finishedWork.actualStartTime, commitTime);
            }

            {
              if (typeof onCommit === 'function') {
                onCommit(finishedWork.memoizedProps.id, phase, effectDuration, commitTime);
              } // Schedule a passive effect for this Profiler to call onPostCommit hooks.
              // This effect should be scheduled even if there is no onPostCommit callback for this Profiler,
              // because the effect is also where times bubble to parent Profilers.


              enqueuePendingPassiveProfilerEffect(finishedWork); // Propagate layout effect durations to the next nearest Profiler ancestor.
              // Do not reset these values until the next render so DevTools has a chance to read them first.

              var parentFiber = finishedWork.return;

              outer: while (parentFiber !== null) {
                switch (parentFiber.tag) {
                  case HostRoot:
                    var root = parentFiber.stateNode;
                    root.effectDuration += effectDuration;
                    break outer;

                  case Profiler:
                    var parentStateNode = parentFiber.stateNode;
                    parentStateNode.effectDuration += effectDuration;
                    break outer;
                }

                parentFiber = parentFiber.return;
              }
            }
          }

          break;
        }

      case SuspenseComponent:
        {
          commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
          break;
        }

      case SuspenseListComponent:
      case IncompleteClassComponent:
      case ScopeComponent:
      case OffscreenComponent:
      case LegacyHiddenComponent:
        break;

      default:
        throw new Error('This unit of work tag should not have side-effects. This error is ' + 'likely caused by a bug in React. Please file an issue.');
    }
  }

  if ( !offscreenSubtreeWasHidden) {
    {
      if (finishedWork.flags & Ref) {
        commitAttachRef(finishedWork);
      }
    }
  }
}
  1. 判断当前flags 是否存在LayoutMask副作用
  2. 如果存在LayoutMask副作用,根据当前的 fiber 节点的 tag 进入不同的分支处理。 我们主要分析函数式组件
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent:
        {
          if ( !offscreenSubtreeWasHidden) {
            // At this point layout effects have already been destroyed (during mutation phase).
            // This is done to prevent 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 ( finishedWork.mode & ProfileMode) {
              try {
                startLayoutEffectTimer();
                commitHookEffectListMount(Layout | HasEffect, finishedWork);
              } finally {
                recordLayoutEffectDuration(finishedWork);
              }
            } else {
              commitHookEffectListMount(Layout | HasEffect, finishedWork);
            }
          }

          break;
        }

如果是函数式组件的话,进入commitHookEffectListMount。

commitHookEffectListMount
function commitHookEffectListMount(flags, finishedWork) {
  var updateQueue = finishedWork.updateQueue;
  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

  if (lastEffect !== null) {
    var firstEffect = lastEffect.next;
    var effect = firstEffect;

    do {
      if ((effect.tag & flags) === flags) {
        {
          if ((flags & Passive$1) !== NoFlags$1) {
            markComponentPassiveEffectMountStarted(finishedWork);
          } else if ((flags & Layout) !== NoFlags$1) {
            markComponentLayoutEffectMountStarted(finishedWork);
          }
        } // Mount


        var create = effect.create;
        effect.destroy = create();

        {
          if ((flags & Passive$1) !== NoFlags$1) {
            markComponentPassiveEffectMountStopped();
          } else if ((flags & Layout) !== NoFlags$1) {
            markComponentLayoutEffectMountStopped();
          }
        }

        {
          var destroy = effect.destroy;

          if (destroy !== undefined && typeof destroy !== 'function') {
            var hookName = void 0;

            if ((effect.tag & Layout) !== NoFlags) {
              hookName = 'useLayoutEffect';
            } else if ((effect.tag & Insertion) !== NoFlags) {
              hookName = 'useInsertionEffect';
            } else {
              hookName = 'useEffect';
            }

            var addendum = void 0;

            if (destroy === null) {
              addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).';
            } else if (typeof destroy.then === 'function') {
              addendum = '\n\nIt looks like you wrote ' + hookName + '(async () => ...) or returned a Promise. ' + 'Instead, write the async function inside your effect ' + 'and call it immediately:\n\n' + hookName + '(() => {\n' + '  async function fetchData() {\n' + '    // You can await here\n' + '    const response = await MyAPI.getData(someId);\n' + '    // ...\n' + '  }\n' + '  fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching';
            } else {
              addendum = ' You returned: ' + destroy;
            }

            error('%s must not return anything besides a function, ' + 'which is used for clean-up.%s', hookName, addendum);
          }
        }
      }

      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

updateQueue 是存储副作用的地方,可以看一下updateQueue 的数据结构是怎么样的:

image.png 其中 create 就是我们定义的 useLayoutEffecct 中的函数。当我们有多个副作用函数时,会通过 next 进行连接,这里不展开讲了,后续讲副作用的时候会补这方面的知识。

do {
      if ((effect.tag & flags) === flags) {
        {
          if ((flags & Passive$1) !== NoFlags$1) {
            markComponentPassiveEffectMountStarted(finishedWork);
          } else if ((flags & Layout) !== NoFlags$1) {
            markComponentLayoutEffectMountStarted(finishedWork);
          }
        } // Mount


        var create = effect.create;
        effect.destroy = create();

        {
          if ((flags & Passive$1) !== NoFlags$1) {
            markComponentPassiveEffectMountStopped();
          } else if ((flags & Layout) !== NoFlags$1) {
            markComponentLayoutEffectMountStopped();
          }
        }

        {
          var destroy = effect.destroy;

          if (destroy !== undefined && typeof destroy !== 'function') {
            var hookName = void 0;

            if ((effect.tag & Layout) !== NoFlags) {
              hookName = 'useLayoutEffect';
            } else if ((effect.tag & Insertion) !== NoFlags) {
              hookName = 'useInsertionEffect';
            } else {
              hookName = 'useEffect';
            }

            var addendum = void 0;

            if (destroy === null) {
              addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).';
            } else if (typeof destroy.then === 'function') {
              addendum = '\n\nIt looks like you wrote ' + hookName + '(async () => ...) or returned a Promise. ' + 'Instead, write the async function inside your effect ' + 'and call it immediately:\n\n' + hookName + '(() => {\n' + '  async function fetchData() {\n' + '    // You can await here\n' + '    const response = await MyAPI.getData(someId);\n' + '    // ...\n' + '  }\n' + '  fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching';
            } else {
              addendum = ' You returned: ' + destroy;
            }

            error('%s must not return anything besides a function, ' + 'which is used for clean-up.%s', hookName, addendum);
          }
        }
      }

      effect = effect.next;
    } while (effect !== firstEffect);

这个函数是具体执行副作用的地方,首先判断updateQueue是否为 null。 如果不等于null 的情况下,会依次执行副作用。同时循环的结束条件是整个链表全部执行完毕,因为这个链表是一个环状链表,所以判断结束的条件是 effect === firstEffect。

总结

commit 阶段是将 React 构造出来的树渲染到屏幕上的阶段,首先会进行两次副作用判断,一次是判断子孙是否有副作用,另一次是判断自己是否有副作用。 如果有副作用就会进入三个阶段,分别是commitBeforeMutationEffects,commitMutationEffects,commitLayoutEffects,requestPaint

commitBeforeMutationEffects 主要的作用是通过判断当前的 flags是否存在Snapshot 副作用。如果存在副作用,根据 fiber 的 tag 是 class 还是函数式组件,如果是 class,会调用getSnapshotBeforeUpdate。如果是函数式组件,会 clear 容器的一些内容。

commitMutationEffects主要作用是判断当前是否有要删除的副作用fiber,如果 fiber 有删除的节点。 先进行删除操作,同时会将真实 dom 节点插入到指定的位置。

commitLayoutEffects主要是判断当前是否有 useLayoutEffect 副作用,如果有副作用,执行副作用函数,并在合适的时机执行cleanUp清理函数。