前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React源码解析之Commit第二子阶段「mutation」(下)

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

作者头像
进击的小进进
发布2020-04-27 19:43:36
8210
发布2020-04-27 19:43:36
举报
文章被收录于专栏:前端干货和生活感悟

前言

在上篇文章 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


(完)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-04-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 webchen 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、commitDeletion()
  • 二、unmountHostComponents()
  • 三、commitNestedUnmounts()
  • 四、commitUnmount()
  • 流程图
  • GitHub
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档