React16源碼解析(八)-更新流程提交階段

React源碼解析系列文章歡迎閱讀:
React16源碼解析(一)- 圖解Fiber架構
React16源碼解析(二)-創建更新
React16源碼解析(三)-ExpirationTime
React16源碼解析(四)-Scheduler
React16源碼解析(五)-更新流程渲染階段1
React16源碼解析(六)-更新流程渲染階段2
React16源碼解析(七)-更新流程渲染階段3
React16源碼解析(八)-更新流程提交階段
正在更新中...

提交階段相比於渲染階段要簡單很多,因爲大部分更新的前期操作都在渲染階段做好了。提交階段的主要任務也就是把之前記錄好的更新操作反映到真實的dom上,並且這個過程是不能中斷的。

commitRoot

1、檢查 finishedWork 是否也有 effect ,如有插入 effect 鏈表中
2、第一次遍歷effect鏈,更新class組件實例上的state,props,執行getSnapshotBeforeUpdate生命週期
3、第二次遍歷effect鏈,不同的effectTag,執行不同的操作,比如重置文本節點,執行 插入、更新、刪除等的 effect 操作,真正的對 dom 進行修改。
4、第三次遍歷effect鏈,這次遍歷就是做一些收尾工作。執行componentDidMount、componentDidUpdate,更新的回調函數等。
5、做一些 還原變量 等的收尾工作。

下面會着重講解effect鏈表的三次遍歷。

function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
  isWorking = true;
  isCommitting = true;
  // ......

  // 檢查 finishedWork 是否也有 effect ,如有插入 effect 鏈表中
  let firstEffect;
  if (finishedWork.effectTag > PerformedWork) {
    // A fiber's effect list consists only of its children, not itself. So if
    // the root has an effect, we need to add it to the end of the list. The
    // resulting list is the set that would belong to the root's parent, if
    // it had one; that is, all the effects in the tree including the root.
    if (finishedWork.lastEffect !== null) {
      finishedWork.lastEffect.nextEffect = finishedWork;
      firstEffect = finishedWork.firstEffect;
    } else {
      firstEffect = finishedWork;
    }
  } else {
    // There is no effect on the root.
    firstEffect = finishedWork.firstEffect;
  }

  prepareForCommit(root.containerInfo);

  // Invoke instances of getSnapshotBeforeUpdate before mutation.
  nextEffect = firstEffect;
  startCommitSnapshotEffectsTimer();
  // 第一次遍歷
  while (nextEffect !== null) {
    let didError = false;
    let error;
    commitBeforeMutationLifecycles();
    // ......
  }
  // ......
  // Commit all the side-effects within a tree. We'll do this in two passes.
  // The first pass performs all the host insertions, updates, deletions and
  // ref unmounts.
  nextEffect = firstEffect;
  startCommitHostEffectsTimer();
  // 第二次遍歷
  while (nextEffect !== null) {
    let didError = false;
    let error;
    // ......
    commitAllHostEffects();
    // ......
  }
  stopCommitHostEffectsTimer();

  resetAfterCommit(root.containerInfo);

  // The work-in-progress tree is now the current tree. This must come after
  // the first pass of the commit phase, so that the previous tree is still
  // current during componentWillUnmount, but before the second pass, so that
  // the finished work is current during componentDidMount/Update.
  root.current = finishedWork;

  // In the second pass we'll perform all life-cycles and ref callbacks.
  // Life-cycles happen as a separate pass so that all placements, updates,
  // and deletions in the entire tree have already been invoked.
  // This pass also triggers any renderer-specific initial effects.
  nextEffect = firstEffect;
  startCommitLifeCyclesTimer();
  // 第三次遍歷
  while (nextEffect !== null) {
    commitAllLifeCycles(root, committedExpirationTime);
  }

  // 下面做一些 還原變量 等的收尾工作
  isCommitting = false;
  isWorking = false;
  stopCommitLifeCyclesTimer();
  stopCommitTimer();
  onCommitRoot(finishedWork.stateNode);

  const updateExpirationTimeAfterCommit = finishedWork.expirationTime;
  const childExpirationTimeAfterCommit = finishedWork.childExpirationTime;
  const earliestRemainingTimeAfterCommit =
    updateExpirationTimeAfterCommit === NoWork ||
    (childExpirationTimeAfterCommit !== NoWork &&
      childExpirationTimeAfterCommit < updateExpirationTimeAfterCommit)
      ? childExpirationTimeAfterCommit
      : updateExpirationTimeAfterCommit;
  if (earliestRemainingTimeAfterCommit === NoWork) {
    // If there's no remaining work, we can clear the set of already failed
    // error boundaries.
    legacyErrorBoundariesThatAlreadyFailed = null;
  }
  onCommit(root, earliestRemainingTimeAfterCommit);
  // ......
}

第一次遍歷-commitBeforeMutationLifecycles

遍歷effect鏈
在這個循環中,組件的state已經更新,但是節點還沒有更新。

function commitBeforeMutationLifecycles() {
  while (nextEffect !== null) {

    const effectTag = nextEffect.effectTag;
    if (effectTag & Snapshot) {
      recordEffect();
      const current = nextEffect.alternate;
      // 這裏調用的是下面的方法,名字一樣
      commitBeforeMutationLifeCycles(current, nextEffect);
    }

    // Don't cleanup effects yet;
    // This will be done by commitAllLifeCycles()
    nextEffect = nextEffect.nextEffect;
  }
}

以下方法做的主要事情:
1、在 class 組件中通過 prevProps, prevState 獲取狀態快照,用於 componentDidUpdate 生命週期
2、狀態快照的獲取通過 getSnapshotBeforeUpdate 生命週期執行後的返回值
commitBeforeMutationLifeCycles 中只有在更新任務是 classComponent 時纔有工作

function commitBeforeMutationLifeCycles(
  current: Fiber | null,
  finishedWork: Fiber,
): void {
  switch (finishedWork.tag) {
    case ClassComponent: {
      if (finishedWork.effectTag & Snapshot) {
        if (current !== null) {
          // 不是初次加載
          // 組件初次加載執行 DidMount 生命週期函數不走 DidUpdate 不需要保存快照對象
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          startPhaseTimer(finishedWork, 'getSnapshotBeforeUpdate');
          // 得到當前class組件的實例
          const instance = finishedWork.stateNode;
          // 更新實例上的props和state
          instance.props = finishedWork.memoizedProps;
          instance.state = finishedWork.memoizedState;
          // 調用getSnapshotBeforeUpdate生命週期方法
          const snapshot = instance.getSnapshotBeforeUpdate(
            prevProps,
            prevState,
          );
          // 保存到instance.__reactInternalSnapshotBeforeUpdate 給 DidUpdate生命週期方法使用
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
          stopPhaseTimer();
        }
      }
      return;
    }
    case HostRoot:
    case HostComponent:
    case HostText:
    case HostPortal:
    case IncompleteClassComponent:
      // Nothing to do for these component types
      return;
    default: {
      // ......
    }
  }
}

第二次遍歷-commitAllHostEffects

1、遍歷effect鏈
2、不同的effectTag,執行不同的操作,比如重置文本節點,執行 插入、更新、刪除等的 effect 操作,真正的對 dom 進行修改。

下面我開始講解三種節點的操作:
1、插入節點 - commitPlacement
2、更新節點 - commitWork
3、刪除節點 - commitDeletion

function commitAllHostEffects() {
  // 遍歷effect鏈
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;

    // 重置文本節點
    if (effectTag & ContentReset) {
      commitResetTextContent(nextEffect);
    }

    if (effectTag & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        commitDetachRef(current);
      }
    }

    // The following switch statement is only concerned about placement,
    // updates, and deletions. To avoid needing to add a case for every
    // possible bitmap value, we remove the secondary effects from the
    // effect tag and switch on that value.
    let primaryEffectTag = effectTag & (Placement | Update | Deletion);
    switch (primaryEffectTag) {
      case Placement: {
        // 插入節點
        commitPlacement(nextEffect);
        // Clear the "placement" from effect tag so that we know that this is inserted, before
        // any life-cycles like componentDidMount gets called.
        // TODO: findDOMNode doesn't rely on this any more but isMounted
        // does and isMounted is deprecated anyway so we should be able
        // to kill this.
        // 刪除effectTag
        nextEffect.effectTag &= ~Placement;
        break;
      }
      case PlacementAndUpdate: {
        // 插入並且更新
        // 插入節點
        commitPlacement(nextEffect);
        // Clear the "placement" from effect tag so that we know that this is inserted, before
        // any life-cycles like componentDidMount gets called.
        nextEffect.effectTag &= ~Placement;

        //  更新節點
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      case Update: {
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      case Deletion: {
        // 刪除節點
        commitDeletion(nextEffect);
        break;
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}

commitPlacement

1、找到 finishedWork 的父節點 parentFiber。尋找的是原生的dom節點對應的fiber。如果父級不是原生dom,則繼續往上尋找。
2、得到parentFiber對應的原生dom節點parent
3、找到插入節點的後一個節點,因爲我要插在它前面
4、用insertBefore或者appendChild進行子節點插入操作

注:第4步插入操作需要分情況,比如如果是原生dom節點是直接插入,如果是Class子節點是需要深度優先遍歷子節點進行插入的。

function commitPlacement(finishedWork: Fiber): void {
  if (!supportsMutation) {
    return;
  }

  // Recursively insert all host nodes into the parent.
  // 找到 finishedWork 的父節點 parentFiber。尋找的是原生的dom節點對應的fiber。如果父級不是原生dom,則繼續往上尋找
  // 所以parentFiber只有三種類型的節點:HostComponent、HostRoot、HostPortal
  const parentFiber = getHostParentFiber(finishedWork);

  // Note: these two variables *must* always be updated together.
  let parent;
  let isContainer;

  // 得到原生dom節點 : parent
  switch (parentFiber.tag) {
    case HostComponent:
      parent = parentFiber.stateNode;
      isContainer = false;
      break;
    case HostRoot:
      parent = parentFiber.stateNode.containerInfo;
      isContainer = true;
      break;
    case HostPortal:
      parent = parentFiber.stateNode.containerInfo;
      isContainer = true;
      break;
    default:
      // ......
  }
  if (parentFiber.effectTag & ContentReset) {
    // Reset the text content of the parent before doing any insertions
    resetTextContent(parent);
    // Clear ContentReset from the effect tag
    parentFiber.effectTag &= ~ContentReset;
  }

  // 這裏操作 dom 使用的是 insertBefore 原生api
  // 所以需要得到插入節點的後一個節點,因爲我要插在它前面
  const before = getHostSibling(finishedWork);
  // We only have the top Fiber that was inserted but we need recurse down its
  // children to find all the terminal nodes.
  let node: Fiber = finishedWork;
  while (true) {
    // 如果當前節點是原始dom節點就直接進行插入
    if (node.tag === HostComponent || node.tag === HostText) {
      if (before) {
        // 有before就用insertBefore
        if (isContainer) {
          insertInContainerBefore(parent, node.stateNode, before);
        } else {
          insertBefore(parent, node.stateNode, before);
        }
      } else {
        // 沒有before就用appendChild
        if (isContainer) {
          appendChildToContainer(parent, node.stateNode);
        } else {
          appendChild(parent, node.stateNode);
        }
      }
    } else if (node.tag === HostPortal) {
      // 是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 if (node.child !== null) {
      // 上面條件不滿足其實就是class組件
      // 查看子節點是否存在,存在的話把子節點進行插入 continue
      node.child.return = node;
      node = node.child;
      continue;
    }
    if (node === finishedWork) {
      // 對finishedWork已經結束
      return;
    }
    // 其實這是一個深度優先遍歷
    // 深度優先遍歷class組件的子節點,把class組件的原生dom節點全部進行插入操作
    // 下面是深度優先遍歷的退回過程,如果沒有兄弟節點,就往上退回
    while (node.sibling === null) {
      if (node.return === null || node.return === finishedWork) {
        return;
      }
      node = node.return;
    }
    node.sibling.return = node.return;
    node = node.sibling;
  }
}

getHostSibling:
這個函數其實是找我要插入的節點(finishedWork)的下一個dom節點,因爲我要插在這個節點前面嘛。所以這個函數做的事情就是:剔除掉所有的非原始dom節點,找到我想要的dom節點。
注:HostPortal也是要被剔除的,因爲它不是掛載在這個地方的嘛。

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;
  // 外層while循環
  siblings: while (true) {
    // If we didn't find anything, let's try the next sibling.
    // 如果沒有兄弟節點,向上查找父節點,但是這個父節點不能是原生dom節點
    while (node.sibling === null) {
      if (node.return === null || isHostParent(node.return)) {
        // 如果到了根節點root了 或者 是原生dom節點  返回 null 說明在真實的dom中 插入的這個節點沒有兄弟節點
        // If we pop out of the root or hit the parent the fiber we are the
        // last sibling.
        return null;
      }
      node = node.return;
    }
    // 下面是有兄弟節點的情況
    node.sibling.return = node.return;
    node = node.sibling;
    // 兄弟節點不是HostComponent也不是HostText
    while (node.tag !== HostComponent && node.tag !== HostText) {
      // 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.effectTag & 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也跳過這個節點查找下一個兄弟節點
      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.effectTag & Placement)) {
      // Found it!
      return node.stateNode;
    }
  }
}

commitWork

1、commitWork 只會更新 HostComponent(dom 節點) 和 HostText(文本節點)
2、HostComponent調用commitUpdate更新
3、HostText調用commitTextUpdate更新

function commitWork(current: Fiber | null, finishedWork: Fiber): void {
  // ......

  switch (finishedWork.tag) {
    case ClassComponent: {
      return;
    }
    case HostComponent: {
      // 更新dom標籤節點
      const instance: Instance = finishedWork.stateNode;
      if (instance != null) {
        // Commit the work prepared earlier.
        // 提取新的props
        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.
        // 提取老的props
        const oldProps = current !== null ? current.memoizedProps : newProps;
        const type = finishedWork.type;
        // TODO: Type the updateQueue to be specific to host components.
        // 取出updateQueue
        const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
        // 清空updateQueue
        finishedWork.updateQueue = null;
        if (updatePayload !== null) {
          // 進行更新
          commitUpdate(
            instance,
            updatePayload,
            type,
            oldProps,
            newProps,
            finishedWork,
          );
        }
      }
      return;
    }
    case HostText: {
      // 更新文本節點
      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;
      commitTextUpdate(textInstance, oldText, newText);
      return;
    }
    case HostRoot: {
      return;
    }
    case Profiler: {
      return;
    }
    case SuspenseComponent: {
      return;
    }
    case IncompleteClassComponent: {
      return;
    }
    default: {
      // ......
    }
  }
}

commitUpdate:

export function commitUpdate(
  domElement: Instance,
  updatePayload: Array<mixed>,
  type: string,
  oldProps: Props,
  newProps: Props,
  internalInstanceHandle: Object,
): void {
  // Update the props handle so that we know which props are the ones with
  // with current event handlers.
  // 這裏只是: domElement[__reactEventHandlers$] = newProps
  updateFiberProps(domElement, newProps);

  // Apply the diff to the DOM node.
  updateProperties(domElement, updatePayload, type, oldProps, newProps);
}

updateProperties:
1、針對特殊的標籤有特殊的處理,比如表單
2、調用updateDOMProperties把之前的diff結果應用的真實dom上面

// Apply the diff.
export function updateProperties(
  domElement: Element,
  updatePayload: Array<any>,
  tag: string,
  lastRawProps: Object,
  nextRawProps: Object,
): void {
  // Update checked *before* name.
  // In the middle of an update, it is possible to have multiple checked.
  // When a checked radio tries to change name, browser makes another radio's checked false.
  // 針對表單的特殊標籤
  if (
    tag === 'input' &&
    nextRawProps.type === 'radio' &&
    nextRawProps.name != null
  ) {
    ReactDOMInput.updateChecked(domElement, nextRawProps);
  }

  // 判斷是否是自定義標籤 即標籤名中有 ‘-’
  const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
  const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
  // Apply the diff.
  // 把之前的diff結果應用的真實dom上面
  updateDOMProperties(
    domElement,
    updatePayload,
    wasCustomComponentTag,
    isCustomComponentTag,
  );

  // TODO: Ensure that an update gets scheduled if any of the special props
  // changed.
  // 針對表單做一些特殊的處理
  switch (tag) {
    case 'input':
      // Update the wrapper around inputs *after* updating props. This has to
      // happen after `updateDOMProperties`. Otherwise HTML5 input validations
      // raise warnings and prevent the new value from being assigned.
      ReactDOMInput.updateWrapper(domElement, nextRawProps);
      break;
    case 'textarea':
      ReactDOMTextarea.updateWrapper(domElement, nextRawProps);
      break;
    case 'select':
      // <select> value update needs to occur after <option> children
      // reconciliation
      ReactDOMSelect.postUpdateWrapper(domElement, nextRawProps);
      break;
  }
}

updateDOMProperties:
1、把我們之前渲染階段生成的updatePayload: [k1,null,k2,v2,k3,v3]應用到真實的dom上。
2、對一些dom節點上特殊的屬性做特殊的處理,比如style、dangerouslySetInnerHTML等

function updateDOMProperties(
  domElement: Element,
  updatePayload: Array<any>,
  wasCustomComponentTag: boolean,
  isCustomComponentTag: boolean,
): void {
  // TODO: Handle wasCustomComponentTag
  // 遍歷updatePayload
  for (let i = 0; i < updatePayload.length; i += 2) {
    // 取出key 和 value
    const propKey = updatePayload[i];
    const propValue = updatePayload[i + 1];
    if (propKey === STYLE) {
      // 如果是樣式更新
      CSSPropertyOperations.setValueForStyles(domElement, propValue);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      // 如果是dangerouslySetInnerHTML
      setInnerHTML(domElement, propValue);
    } else if (propKey === CHILDREN) {
      // 如果是文本
      setTextContent(domElement, propValue);
    } else {
      // 否則正常的更新節點的屬性
      DOMPropertyOperations.setValueForProperty(
        domElement,
        propKey,
        propValue,
        isCustomComponentTag,
      );
    }
  }
}

commitTextUpdate:
文本節點的更新就非常簡單了

export function commitTextUpdate(
  textInstance: TextInstance,
  oldText: string,
  newText: string,
): void {
  textInstance.nodeValue = newText;
}

commitDeletion

在我們刪除一個節點時候,你可能簡單的想一下,就把一個節點刪除就好了呀。其實沒這麼簡單哦。
刪除一個節點時候,是需要去遍歷整顆子樹的。第一,如果dom節點下面還有class組件,那麼我們是要調用它的生命週期方法的(componentWillUnmount),第二,如果有HostPortal,那麼還要去刪除HostPortal中的節點,所以我們必須要遍歷子樹的。這裏的遍歷採用樹的深度優先遍歷。

react用了三個函數用循環+遞歸深度優先遍歷了整顆子樹。

function commitDeletion(current: Fiber): void {
  // Recursively delete all host nodes from the parent.
  // Detach refs and call componentWillUnmount() on the whole subtree.
  unmountHostComponents(current);
  // ......
  detachFiber(current);
}

unmountHostComponents:

function unmountHostComponents(current): void {
  // We only have the top Fiber that was deleted but we need recurse down its
  // children to find all the terminal nodes.
  let node: Fiber = current;

  // Each iteration, currentParent is populated with node's host parent if not
  // currentParentIsValid.
  let currentParentIsValid = false;

  // Note: these two variables *must* always be updated together.
  let currentParent;
  let currentParentIsContainer;

  while (true) {
    // 找的父節點,這個父節點一定是個dom節點
    if (!currentParentIsValid) {
      let parent = node.return;
      findParent: while (true) {
        invariant(
          parent !== null,
          'Expected to find a host parent. This error is likely caused by ' +
            'a bug in React. Please file an issue.',
        );
        switch (parent.tag) {
          case HostComponent:
            currentParent = parent.stateNode;
            currentParentIsContainer = false;
            break findParent;
          case HostRoot:
            currentParent = parent.stateNode.containerInfo;
            currentParentIsContainer = true;
            break findParent;
          case HostPortal:
            currentParent = parent.stateNode.containerInfo;
            currentParentIsContainer = true;
            break findParent;
        }
        parent = parent.return;
      }
      currentParentIsValid = true;
    }

    if (node.tag === HostComponent || node.tag === HostText) {
      // 如果是原生dom節點
      commitNestedUnmounts(node);
      // After all the children have unmounted, it is now safe to remove the
      // node from the tree.
      if (currentParentIsContainer) {
        removeChildFromContainer((currentParent: any), node.stateNode);
      } else {
        removeChild((currentParent: any), node.stateNode);
      }
      // Don't visit children because we already visited them.
    } else if (node.tag === HostPortal) {
      // 如果是HostPortal不會做什麼操作,直接向下遍歷子節點  因爲它沒有ref,也沒有生命週期
      // When we go into a portal, it becomes the parent to remove from.
      // We will reassign it back when we pop the portal on the way up.
      currentParent = node.stateNode.containerInfo;
      currentParentIsContainer = true;
      // Visit children because portals might contain host components.
      if (node.child !== null) {
        node.child.return = node;
        node = node.child;
        continue;
      }
    } else {
      // 到這裏的話,其實就是react組件節點了,調用commitUnmount,這個方法裏會有生命週期的調用,ref的卸載
      commitUnmount(node);
      // Visit children because we may find more host components below.
      // 繼續遍歷子節點
      if (node.child !== null) {
        node.child.return = node;
        node = node.child;
        continue;
      }
    }
    // 整棵樹遍歷完成
    if (node === current) {
      return;
    }
    // 如果沒有兄弟節點,深度優先遍歷可以向父節點返回了
    while (node.sibling === null) {
      if (node.return === null || node.return === current) {
        return;
      }
      node = node.return;
      if (node.tag === HostPortal) {
        // When we go out of the portal, we need to restore the parent.
        // Since we don't keep a stack of them, we will search for it.
        currentParentIsValid = false;
      }
    }
    // 到這裏代表已經沒有子節點了,就向兄弟節點方向遍歷
    node.sibling.return = node.return;
    node = node.sibling;
  }
}

commitNestedUnmounts:
這個方法使用深度優先遍歷整顆dom節點爲父節點的樹。子節點還有可能是react組件或者HostPortal。
這裏對每個節點都要調用commitUnmount。如果遇到了HostPortal就會停止對它下面的子樹進行遍歷,因爲在commitUnmount中會對HostPortal類型有個特殊的處理。下面再看。

function commitNestedUnmounts(root: Fiber): void {
  // While we're inside a removed host node we don't want to call
  // removeChild on the inner nodes because they're removed by the top
  // call anyway. We also want to call componentWillUnmount on all
  // composites before this host node is removed from the tree. Therefore
  // we do an inner loop while we're still inside the host node.
  let node: Fiber = root;
  while (true) {
    commitUnmount(node);
    // 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)
    ) {
      // 如果是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;
  }
}

commitUnmount:
這個方法會對不同的節點做不同的處理,對ClassComponent會執行它的生命週期以及卸載ref,對HostComponent卸載ref,對HostPortal重新調用unmountHostComponents,這裏我們就明白了爲什麼上一個方法遇到HostPortal就停止了對它的子樹的遍歷,因爲它會重新遞歸調用unmountHostComponents遍歷子樹。

function commitUnmount(current: Fiber): void {
  onCommitUnmount(current);

  switch (current.tag) {
    case ClassComponent: {
      // 卸載ref
      safelyDetachRef(current);
      const instance = current.stateNode;
      // 執行WillUnmount方法,這個時候真實dom還沒有卸載,即將卸載
      if (typeof instance.componentWillUnmount === 'function') {
        safelyCallComponentWillUnmount(current, instance);
      }
      return;
    }
    case HostComponent: {
      // 卸載ref
      safelyDetachRef(current);
      return;
    }
    case HostPortal: {
      // TODO: this is recursive.
      // We are also not using this parent because
      // the portal will get pushed immediately.
      // 遞歸調用,遍歷子樹
      unmountHostComponents(current);
      // ......
      return;
    }
  }
}

第三次遍歷-commitAllLifeCycles

這次遍歷就是做一些收尾工作。

1、首次渲染執行componentDidMount
2、更新渲染執行 componentDidUpdate
3、執行 setState 的 callback 回調函數
4、清空commitUpdateQueue

function commitAllLifeCycles(
  finishedRoot: FiberRoot,
  committedExpirationTime: ExpirationTime,
) {
  // 循環effect鏈
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;

    // 有更新或者有回調函數
    if (effectTag & (Update | Callback)) {
      recordEffect();
      const current = nextEffect.alternate;
      commitLifeCycles(
        finishedRoot,
        current,
        nextEffect,
        committedExpirationTime,
      );
    }

    if (effectTag & Ref) {
      recordEffect();
      commitAttachRef(nextEffect);
    }

    const next = nextEffect.nextEffect;
    // Ensure that we clean these up so that we don't accidentally keep them.
    // I'm not actually sure this matters because we can't reset firstEffect
    // and lastEffect since they're on every node, not just the effectful
    // ones. So we have to clean everything as we reuse nodes anyway.
    nextEffect.nextEffect = null;
    // Ensure that we reset the effectTag here so that we can rely on effect
    // tags to reason about the current life-cycle.
    nextEffect = next;
  }
}

commitLifeCycles:
這個方法會執行componentDidMount或者componentDidUpdate生命週期方法,最後調用commitUpdateQueue。

function commitLifeCycles(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedExpirationTime: ExpirationTime,
): void {
  switch (finishedWork.tag) {
    case ClassComponent: {
      const instance = finishedWork.stateNode;
      if (finishedWork.effectTag & Update) {
        if (current === null) {
          // 首次渲染
          startPhaseTimer(finishedWork, 'componentDidMount');
          instance.props = finishedWork.memoizedProps;
          instance.state = finishedWork.memoizedState;
          instance.componentDidMount();
          stopPhaseTimer();
        } else {
          // 組件更新
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          startPhaseTimer(finishedWork, 'componentDidUpdate');
          instance.props = finishedWork.memoizedProps;
          instance.state = finishedWork.memoizedState;
          instance.componentDidUpdate(
            prevProps,
            prevState,
            instance.__reactInternalSnapshotBeforeUpdate,
          );
          stopPhaseTimer();
        }
      }
      const updateQueue = finishedWork.updateQueue;
      if (updateQueue !== null) {
        instance.props = finishedWork.memoizedProps;
        instance.state = finishedWork.memoizedState;
        commitUpdateQueue(
          finishedWork,
          updateQueue,
          instance,
          committedExpirationTime,
        );
      }
      return;
    }
    case HostRoot: {
      const updateQueue = finishedWork.updateQueue;
      if (updateQueue !== null) {
        let 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;
          }
        }
        // 因爲ReactDOM.render也有回調函數  所以也要調用commitUpdateQueue
        commitUpdateQueue(
          finishedWork,
          updateQueue,
          instance,
          committedExpirationTime,
        );
      }
      return;
    }
    case HostComponent: {
      const instance: Instance = 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.effectTag & Update) {
        const type = finishedWork.type;
        const props = finishedWork.memoizedProps;
        // 這個函數只是對input標籤有 auto-focus 的情況進行處理
        commitMount(instance, type, props, finishedWork);
      }

      return;
    }
    case HostText: ......
    case HostPortal: ......
    case Profiler: ......
    case SuspenseComponent: ......
    case IncompleteClassComponent:......
    default: {
      // ......
    }
  }
}

commitUpdateQueue:
1、調用commitUpdateEffects
2、清空commitUpdateQueue

export function commitUpdateQueue<State>(
  finishedWork: Fiber,
  finishedQueue: UpdateQueue<State>,
  instance: any,
  renderExpirationTime: ExpirationTime,
): void {
  // ......
  // Commit the effects
  commitUpdateEffects(finishedQueue.firstEffect, instance);
  // 清空commitUpdateQueue
  finishedQueue.firstEffect = finishedQueue.lastEffect = null;
  // ......
}

commitUpdateEffects:
調用effect上的回調函數。

function commitUpdateEffects<State>(
  effect: Update<State> | null,
  instance: any,
): void {
  while (effect !== null) {
    const callback = effect.callback;
    if (callback !== null) {
      // 調用回調函數,也就是setState的回調函數
      effect.callback = null;
      callCallback(callback, instance);
    }
    effect = effect.nextEffect;
  }
}

任世界紛繁複雜,仍舊保持可愛。
我是小柚子小仙女。文章如有不妥,歡迎指正~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章