React源碼解析之Commit第二子階段「mutation」(下)

前言
在上篇文章 React源碼解析之Commit第二子階段「mutation」(中) 中,咱們講了mutation子階段的更新(Update)操做,接下來咱們講刪除(Deletion)操做:javascript

      case Deletion: {
        //刪除節點
        commitDeletion(nextEffect);
        break;
      }
複製代碼

1、commitDeletion()
做用:
刪除 DOM 節點php

源碼:java

function commitDeletion(current: Fiber): void {
  //由於是 DOM 操做,因此supportsMutation爲 true
  if (supportsMutation) {
    // Recursively delete all host nodes from the parent.
    // Detach refs and call componentWillUnmount() on the whole subtree.

    //刪除該節點的時候,還會刪除子節點
    //若是子節點是 ClassComponent 的話,須要執行生命週期 API——componentWillUnmount()
    unmountHostComponents(current);
  } else {
    // Detach refs and call componentWillUnmount() on the whole subtree.
    //卸載 ref
    commitNestedUnmounts(current);
  }
  //重置 fiber 屬性
  detachFiber(current);
}
複製代碼

解析:
(1) 執行unmountHostComponents(),刪除目標節點及其子節點,若是目標節點或子節點是類組件ClassComponent的話,會執行內部的生命週期 API——componentWillUnmount()node

(2) 執行detachFiber(),重置fiber屬性react

detachFiber()的源碼以下:git

//重置 fiber 對象,釋放內存(注意是屬性值置爲 null,不會刪除屬性)
function detachFiber(current: Fiber) {
  // Cut off the return pointers to disconnect it from the tree. Ideally, we
  // should clear the child pointer of the parent alternate to let this
  // get GC:ed but we don't know which for sure which parent is the current
  // one so we'll settle for GC:ing the subtree of this child. This child
  // itself will be GC:ed when the parent updates the next time.

  //重置目標 fiber對象,理想狀況下,也應該清除父 fiber的指向(該 fiber),這樣有利於垃圾回收
  //可是 React肯定不了父節點,因此會在目標 fiber 下生成一個子 fiber,表明垃圾回收,該子節點
  //會在父節點更新的時候,成爲垃圾回收
  current.return = null;
  current.child = null;
  current.memoizedState = null;
  current.updateQueue = null;
  current.dependencies = null;
  const alternate = current.alternate;
  //使用的doubleBuffer技術,Fiber在更新後,不用再從新建立對象,而是複製自身,而且二者相互複用,用來提升性能
  //至關因而當前 fiber 的一個副本,用來節省內存用的,也要清空屬性
  if (alternate !== null) {
    alternate.return = null;
    alternate.child = null;
    alternate.memoizedState = null;
    alternate.updateQueue = null;
    alternate.dependencies = null;
  }
}
複製代碼

接下來看下unmountHostComponents()github

2、unmountHostComponents()
做用:
刪除目標節點及其子節點,若是目標節點或子節點是類組件ClassComponent的話,會執行內部的生命週期 API——componentWillUnmount()web

源碼:算法

function unmountHostComponents(current)void {
  // We only have the top Fiber that was deleted but we need to 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) {
    if (!currentParentIsValid) {
      //獲取父節點
      let parent = node.return;
      //將此 while 循環命名爲 findParent
      //此循環的目的是找到是 DOM 類型的父節點
      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:
            //獲取父節點對應的 DOM 元素
            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;
    }
    //若是是 DOM 元素或文本元素的話(主要看這個)
    if (node.tag === HostComponent || node.tag === HostText) {
      //在目標節點被刪除前,從該節點開始深度優先遍歷,卸載 ref 和執行 componentWillUnmount()/effect.destroy()
      commitNestedUnmounts(node);
      // After all the children have unmounted, it is now safe to remove the
      // node from the tree.
      //咱們只看 false 的狀況,也就是操做 DOM 標籤的狀況
      if (currentParentIsContainer) {
        removeChildFromContainer(
          ((currentParent: any): Container),
          (node.stateNode: Instance | TextInstance),
        );
      }

      else {
        //源碼:parentInstance.removeChild(child);
        removeChild(
          ((currentParent: any): Instance),
          (node.stateNode: Instance | TextInstance),
        );
      }
      // Don't visit children because we already visited them.
    }
    //suspense 組件不看
    else if (
      enableSuspenseServerRenderer &&
      node.tag === DehydratedSuspenseComponent
    ) {
      //不看這部分
    }
    //portal 不看
    else if (node.tag === HostPortal) {
      //不看這部分
    }
    //上述狀況都不符合,多是一個 Component 組件
    else {
      //卸載 ref 和執行 componentWillUnmount()/effect.destroy()
      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) {
      //若是遍歷回頂點 或 遍歷完子樹,則直接 return
      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;
  }
}
複製代碼

解析:
咱們仍是隻考慮HostComponentClassCpmonent的狀況,該方法也是一個深度優先遍歷的算法邏輯,因此你必須知道該算法邏輯,才能看得懂while (true) { }裏面作了什麼。數組

關於「ReactDOM裏的深度優先遍歷」請看:
React源碼解析之Commit第二子階段「mutation」(上)中的2、ReactDOM裏的深度優先遍歷

優先遍歷子節點,而後再遍歷兄弟節點
(1) 若是當前節點是DOM 標籤HostComponent或文本節點HostText的話

    if (node.tag === HostComponent || node.tag === HostText) {
複製代碼

① 執行commitNestedUnmounts()

  commitNestedUnmounts(node);
複製代碼

commitNestedUnmounts()的做用是:
在目標節點被刪除前,從該節點開始深度優先遍歷,卸載ref和執行 componentWillUnmount()/effect.destroy()

注意:
commitNestedUnmounts()方法,不會執行removeChild()刪除節點的操做

② 執行removeChild(),刪除當前節點

 removeChild(
          ((currentParent: any): Instance),
          (node.stateNode: Instance | TextInstance),
        );
複製代碼

removeChild()的源碼以下:

export function removeChild(
  parentInstance: Instance,
  child: Instance | TextInstance | SuspenseInstance,
): void 
{
  parentInstance.removeChild(child);
}
複製代碼

就是調用 DOM API——removeChild,請參考:
developer.mozilla.org/zh-CN/docs/…

(2) 若是當前節點是類組件ClassComponent或函數組件FunctionComponent的話(也就是最後的 else 狀況),則執行commitUnmount(),卸載ref和執行componentWillUnmount()/effect.destroy()

   else {
      //卸載 ref 和執行 componentWillUnmount()/effect.destroy()
      commitUnmount(node);
      // Visit children because we may find more host components below.
      if (node.child !== null) {
        node.child.return = node;
        node = node.child;
        continue;
      }
    }
複製代碼

而後就是一直循環,直到調用return,跳出無限循環。

unmountHostComponents()的邏輯其實和commitPlacement()相似,關於commitPlacement(),請看:
React源碼解析之Commit第二子階段「mutation」(上)

接下來,咱們講下commitNestedUnmounts()commitUnmount()源碼

3、commitNestedUnmounts()
做用:
深度優先遍歷,循環執行:
在目標節點被刪除前,從該節點開始深度優先遍歷,卸載該節點及其子節點 ref 和執行該節點及其子節點 componentWillUnmount()/effect.destroy()

源碼:

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.
  //當在被刪除的目標節點的內部時,咱們不想在內部調用removeChild,由於子節點會被父節點給統一刪除
  //可是 React 要在目標節點被刪除的時候,執行componentWillUnmount,這就是commitNestedUnmounts的目的
  let node: Fiber = root;
  while (true) {
    // 卸載 ref 和執行 componentWillUnmount()/effect.destroy()
    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)
    ) {
      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()方法

4、commitUnmount()
做用:
同上

源碼:

function commitUnmount(current: Fiber): void {
  //執行onCommitFiberUnmount(),查了下是個空 function
  onCommitUnmount(current);

  switch (current.tag) {
    //若是是 FunctionComponent 的話
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      //下面代碼結構和[React源碼解析之Commit第一子階段「before mutation」](https://mp.weixin.qq.com/s/YtgEVlZz1i5Yp87HrGrgRA)中的「3、commitHookEffectList()」類似
      //大體思路是循環 effect 鏈,執行每一個 effect 上的 destory()
      const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
      if (updateQueue !== null) {
        const lastEffect = updateQueue.lastEffect;
        if (lastEffect !== null) {
          const firstEffect = lastEffect.next;
          let effect = firstEffect;
          do {
            const destroy = effect.destroy;
            if (destroy !== undefined) {
              //安全(try...catch)執行 effect.destroy()
              safelyCallDestroy(current, destroy);
            }
            effect = effect.next;
          } while (effect !== firstEffect);
        }
      }
      break;
    }
    //若是是 ClassComponent 的話
    case ClassComponent: {
      //安全卸載 ref
      safelyDetachRef(current);
      const instance = current.stateNode;
      //執行生命週期 API—— componentWillUnmount()
      if (typeof instance.componentWillUnmount === 'function') {
        safelyCallComponentWillUnmount(current, instance);
      }
      return;
    }
    //若是是 DOM 標籤的話
    case HostComponent: {
      //安全卸載 ref
      safelyDetachRef(current);
      return;
    }
    //portal 不看
    case HostPortal: {
      // TODO: this is recursive.
      // We are also not using this parent because
      // the portal will get pushed immediately.
      if (supportsMutation) {
        unmountHostComponents(current);
      } else if (supportsPersistence) {
        emptyPortalContainer(current);
      }
      return;
    }
    //事件組件 的更新,暫未找到相關資料
    case EventComponent: {
      if (enableFlareAPI) {
        const eventComponentInstance = current.stateNode;
        unmountEventComponent(eventComponentInstance);
        current.stateNode = null;
      }
    }
  }
}
複製代碼

解析:
主要看三種狀況:
(1) 若是是FunctionComponent的話,則循環updateQueue上的effect鏈,執行每一個effect 上的destory()方法

safelyCallDestroy()源碼以下:

//安全(try...catch)執行 effect.destroy()
function safelyCallDestroy(current, destroy{
  if (__DEV__) {
    //刪除了 dev 代碼
  } else {
    try {
      destroy();
    } catch (error) {
      captureCommitPhaseError(current, error);
    }
  }
}
複製代碼

(2) 若是是ClassComponent的話
① 執行safelyDetachRef(),安全卸載ref

safelyDetachRef()源碼以下:

function safelyDetachRef(current: Fiber{
  const ref = current.ref;
  //ref 不爲 null,若是是 function,則 ref(null),不然 ref.current=null
  if (ref !== null) {
    if (typeof ref === 'function') {
      if (__DEV__) {
        //刪除了 dev 代碼
      } else {
        try {
          ref(null);
        } catch (refError) {
          captureCommitPhaseError(current, refError);
        }
      }
    } else {
      ref.current = null;
    }
  }
}
複製代碼

② 執行safelyCallComponentWillUnmount(),安全調用safelyCallComponentWillUnmount()

safelyCallComponentWillUnmount()源碼以下:

// Capture errors so they don't interrupt unmounting.
//執行生命週期 API—— componentWillUnmount()
function safelyCallComponentWillUnmount(current, instance{
  if (__DEV__) {
    //刪除了 dev 代碼
  } else {
    try {
      //執行生命週期 API—— componentWillUnmount()
      callComponentWillUnmountWithTimer(current, instance);
    } catch (unmountError) {
      captureCommitPhaseError(current, unmountError);
    }
  }
}
複製代碼

callComponentWillUnmountWithTimer()源碼以下:

//執行生命週期 API—— componentWillUnmount()
const callComponentWillUnmountWithTimer = function(current, instance{
  startPhaseTimer(current, 'componentWillUnmount');
  instance.props = current.memoizedProps;
  instance.state = current.memoizedState;
  instance.componentWillUnmount();
  stopPhaseTimer();
};
複製代碼

本質就是調用componentWillUnmount()方法,有一點須要注意的是,執行componentWillUnmount()時,stateprops都是老stateprops

  instance.props = current.memoizedProps;
  instance.state = current.memoizedState;
  instance.componentWillUnmount();
複製代碼

(3) 若是是HostComponent,也就是 DOM 標籤的話,則執行safelyDetachRef(),安全卸載 ref

流程圖

GitHub
commitDeletion()/unmountHostComponents()/commitNestedUnmounts()/commitUnmount()
github.com/AttackXiaoJ…


(完)

相關文章
相關標籤/搜索