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

2020-04-27 19:43:36 浏览数 (1)

前言

在上篇文章 React源码解析之Commit第二子阶段「mutation」(中) 中,我们讲了 mutation 子阶段的更新(Update)操作,接下来我们讲删除(Deletion)操作:

代码语言:javascript复制
      case Deletion: {
        //删除节点
        commitDeletion(nextEffect);
        break;
      }

一、commitDeletion()

作用:删除 DOM 节点

源码:

代码语言:javascript复制
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()

(2) 执行detachFiber(),重置fiber属性

detachFiber()的源码如下:

代码语言:javascript复制
//重置 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()

二、unmountHostComponents()

作用: 删除目标节点及其子节点,如果目标节点或子节点是类组件ClassComponent的话,会执行内部的生命周期 API——componentWillUnmount()

源码:

代码语言:javascript复制
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」(上) 中的 二、ReactDOM里的深度优先遍历

优先遍历子节点,然后再遍历兄弟节点 (1) 如果当前节点是DOM 标签HostComponent或文本节点HostText的话

代码语言:javascript复制
    if (node.tag === HostComponent || node.tag === HostText) {

① 执行commitNestedUnmounts()

代码语言:javascript复制
  commitNestedUnmounts(node);

commitNestedUnmounts()的作用是: 在目标节点被删除前,从该节点开始深度优先遍历,卸载ref和执行 componentWillUnmount()/effect.destroy()

注意: commitNestedUnmounts()方法,不会执行removeChild()删除节点的操作

② 执行removeChild(),删除当前节点

代码语言:javascript复制
 removeChild(
          ((currentParent: any): Instance),
          (node.stateNode: Instance | TextInstance),
        );

removeChild()的源码如下:

代码语言:javascript复制
export function removeChild(
  parentInstance: Instance,
  child: Instance | TextInstance | SuspenseInstance,
): void {
  parentInstance.removeChild(child);
}

就是调用 DOM API——removeChild,请参考: https://developer.mozilla.org/zh-CN/docs/Web/API/Node/removeChild

(2) 如果当前节点是类组件ClassComponent或函数组件FunctionComponent的话(也就是最后的 else 情况),则执行commitUnmount(),卸载ref和执行componentWillUnmount()/effect.destroy()

代码语言:javascript复制
   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()源码

三、commitNestedUnmounts()

作用: 深度优先遍历,循环执行: 在目标节点被删除前,从该节点开始深度优先遍历,卸载该节点及其子节点 ref 和执行该节点及其子节点 componentWillUnmount()/effect.destroy()

源码:

代码语言:javascript复制
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()方法

四、commitUnmount()

作用: 同上

源码:

代码语言:javascript复制
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)中的「三、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()源码如下:

代码语言:javascript复制
//安全(try...catch)执行 effect.destroy()
function safelyCallDestroy(current, destroy) {
  if (__DEV__) {
    //删除了 dev 代码
  } else {
    try {
      destroy();
    } catch (error) {
      captureCommitPhaseError(current, error);
    }
  }
}

(2) 如果是ClassComponent的话 ① 执行safelyDetachRef(),安全卸载ref

safelyDetachRef()源码如下:

代码语言:javascript复制
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()源码如下:

代码语言:javascript复制
// 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()源码如下:

代码语言:javascript复制
//执行生命周期 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

代码语言:javascript复制
  instance.props = current.memoizedProps;
  instance.state = current.memoizedState;
  instance.componentWillUnmount();

(3) 如果是HostComponent,也就是 DOM 标签的话,则执行safelyDetachRef(),安全卸载 ref

流程图

GitHub

commitDeletion()/unmountHostComponents()/commitNestedUnmounts()/commitUnmount()https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberCommitWork.js


(完)

0 人点赞