React原始碼解析之Commit第一子階段「before mutation」

進擊的小進進發表於2020-04-04

前言
在上篇我們介紹了 commitRoot的整體流程,那麼本篇就來介紹它的第一個子階段before mutation

do {
      if (__DEV__) {
        invokeGuardedCallback(null, commitBeforeMutationEffects, null);
        //刪除了 dev 程式碼
      } else {
        try {
          //呼叫 classComponent 上的生命週期方法 getSnapshotBeforeUpdate
          //關於getSnapshotBeforeUpdate,請看:https://zh-hans.reactjs.org/docs/react-component.html#getsnapshotbeforeupdate
          commitBeforeMutationEffects();
        } catch (error) {
          invariant(nextEffect !== null'Should be working on an effect.');
          captureCommitPhaseError(nextEffect, error);
          nextEffect = nextEffect.nextEffect;
        }
      }
    } while (nextEffect !== null);
複製程式碼

一、commitBeforeMutationEffects()
作用:
迴圈effect鏈,對有Snapshoteffect執行commitBeforeMutationEffectOnFiber

原始碼:

function commitBeforeMutationEffects({
  //迴圈 effect 鏈
  while (nextEffect !== null) {
    //如果 effectTag 裡有 Snapshot 這個 effectTag 的話
    //關於&,請看[前端小知識10點(2020.2.10)](https://mp.weixin.qq.com/s/tt2XcW4GF7oBBZOPwTiCcg)中的「8、JS 中的 & 是什麼意思」
    if ((nextEffect.effectTag & Snapshot) !== NoEffect) {
      //dev 可不看
      // setCurrentDebugFiberInDEV(nextEffect);
      //計 effect 的數
      recordEffect();
      //獲取當前 fiber 節點
      const current = nextEffect.alternate;
      commitBeforeMutationEffectOnFiber(current, nextEffect);
      //dev 可不看
      // resetCurrentDebugFiberInDEV();
    }
    nextEffect = nextEffect.nextEffect;
  }
}
複製程式碼

解析:
(1) nextEffecteffect鏈上從firstEffectlastEffect的每一個需要commitfiber物件

(2) 當nextEffect上有Snapshot這個effectTag時,執行commitBeforeMutationEffectOnFiber(),讓不同型別的元件執行不同的操作,來提交(commit)相關effect

二、commitBeforeMutationEffectOnFiber()
作用:
(1) 如果該fiber型別是ClassComponent的話,執行getSnapshotBeforeUpdate生命週期api,將返回的值賦到fiber 物件的__reactInternalSnapshotBeforeUpdate

(2) 如果該fiber型別是FunctionComponent的話,執行hooks上的effect相關 API

(3) 關於&運算子,請看:
前端小知識10點(2020.2.10) 中的8、JS 中的 & 是什麼意思

原始碼:

function commitBeforeMutationLifeCycles(
  current: Fiber | null,
  finishedWork: Fiber,
): void 
{
  switch (finishedWork.tag) {
    //FunctionComponent會執行commitHookEffectList()
    //FunctionComponent是 pureComponent,所以不會有副作用

    //useEffect 和 useLayoutEffect 是賦予FunctionComponent有副作用能力的 hooks
    //useEffect類似於componentDidMount,useLayoutEffect類似於componentDidUpdate
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      //提交 hooks 的 effects
      commitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork);
      return;
    }
    case ClassComponent: {
      if (finishedWork.effectTag & Snapshot) {
        if (current !== null) {
          //老 props
          const prevProps = current.memoizedProps;
          //老 state
          const prevState = current.memoizedState;
          //getSnapshotBeforeUpdate 的計時開始
          startPhaseTimer(finishedWork, 'getSnapshotBeforeUpdate');
          //獲取 classComponent 的例項
          const 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 (__DEV__) {
            //刪除了 dev 程式碼
          }
          //執行 getSnapshotBeforeUpdate 生命週期 api,在元件update前捕獲一些 DOM 資訊,
          //返回自定義的值或 null,統稱為 snapshot
          //關於getSnapshotBeforeUpdate,請參考:https://zh-hans.reactjs.org/docs/react-component.html#getsnapshotbeforeupdate
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
          if (__DEV__) {
            //刪除了 dev 程式碼
          }
          //將 snapshot 賦值到__reactInternalSnapshotBeforeUpdate屬性上,
          // 這種手法跟[React原始碼解析之updateClassComponent(上)](https://mp.weixin.qq.com/s/F_UdPgdt6wtP78eDqUesoA)
          // 中的「三、adoptClassInstance」裡 instance._reactInternalFiber=workInProgress 類似
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
          //getSnapshotBeforeUpdate 的計時結束
          stopPhaseTimer();
        }
      }
      return;
    }
    case HostRoot:
    case HostComponent:
    case HostText:
    case HostPortal:
    case IncompleteClassComponent:
      // Nothing to do for these component types
      return;
    //沒有副作用,不應該進入到 commit 階段
    default: {
      invariant(
        false,
        'This unit of work tag should not have side-effects. This error is ' +
          'likely caused by a bug in React. Please file an issue.',
      );
    }
  }
}
複製程式碼

解析:
(1) 根據fibertag即型別,進行不同的操作。主要看FunctionComponentClassComponent這兩種情況。

(2) 如果是FunctionComponent的話,則執行commitHookEffectList(),該方法稍後解析。

(3) 如果是ClassComponent的話,則執行生命週期方法——getSnapshotBeforeUpdate(),並將返回值賦予給fiber物件上的__reactInternalSnapshotBeforeUpdate屬性上。

也就是說,類元件的子階段before mutation 目的是:
呼叫getSnapshotBeforeUpdate(),在commit前獲取 DOM 相關資訊

(4) getSnapshotBeforeUpdate()在最近一次渲染輸出(提交到 DOM 節點)之前呼叫。

它使得元件能在發生更改之前從 DOM 中捕獲一些資訊(例如,滾動位置)。

此生命週期的任何返回值將作為引數傳遞給componentDidUpdate(),更多詳情請參考:
zh-hans.reactjs.org/docs/react-…

(5)

instance.__reactInternalSnapshotBeforeUpdate = snapshot
複製程式碼

這種手法跟 React原始碼解析之updateClassComponent(上) 中的三、adoptClassInstance裡的

instance._reactInternalFiber=workInProgress
複製程式碼

類似

三、commitHookEffectList()
說明:
useEffect是讓FunctionComponent產生副作用的hooks,當使用useEffect後,會在fiber上的updateQueue.lastEffect生成effect鏈,具體請看ReactFiberHooks.js中的pushEffect()

作用:
迴圈FunctionComponent上的effect鏈,並根據每個effect上的effectTag,執行destroy/create操作(作用類似於componentDidMount/componentWillUnmount

原始碼:

function commitHookEffectList(
  unmountTag: number,
  mountTag: number,
  finishedWork: Fiber,
{
  //FunctionComponent 的更新佇列
  //補充:FunctionComponent的 side-effect 是放在 updateQueue.lastEffect 上的
  //ReactFiberHooks.js中的pushEffect()裡有說明: componentUpdateQueue.lastEffect = effect.next = effect;
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  //如果有副作用 side-effect的話,迴圈effect 鏈,根據 effectTag,執行每個 effect
  if (lastEffect !== null) {
    //第一個副作用
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      //如果包含 unmountTag 這個 effectTag的話,執行destroy(),並將effect.destroy置為 undefined
      if ((effect.tag & unmountTag) !== NoHookEffect) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      //如果包含 mountTag 這個 effectTag 的話,執行 create()
      if ((effect.tag & mountTag) !== NoHookEffect) {
        // Mount
        const create = effect.create;
        effect.destroy = create();

        if (__DEV__) {
          //刪除了 dev 程式碼
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
複製程式碼

解析:
(1) 不同於ClassComponent的 effect 鏈直接取fiber.firstEffect—>fiber.firstEffect.next……,

FunctionComponent的 effect 鏈取的是fiber.updateQueue.lastEffect—>fiber.updateQueue.lastEffect. next….

(2) 迴圈FunctionComponent上的 effect 鏈,
如果effectTagunmountTag的話,就執行effect.destroy()方法,感覺跟componentWillUnmount()作用類似;
如果effectTagmountTag的話,就執行effect.create()方法,感覺跟componentDidMount()作用類似;

總結
before mutation子階段上,
ClassComponent會執行getSnapshotBeforeUpdate(),捕獲 DOM 資訊;
FunctionComponent會執行effect.create()/effect.destory(),類似於componentDidMount()/componentWillUnmount()

流程圖

GitHub
commitBeforeMutationEffects()
github.com/AttackXiaoJ…

commitBeforeMutationLifeCycles()/commitHookEffectList()
github.com/AttackXiaoJ…


(完)

相關文章