React16源码: React中commitAllHostEffects内部的commitDeletion的源码实现

news2024/11/19 0:36:22

commitDeletion


1 )概述

  • 在 react commit 阶段的 commitRoot 第二个while循环中
  • 调用了 commitAllHostEffects,这个函数不仅仅处理了新增节点,更新节点
  • 最后一个操作,就是删除节点,就需要调用 commitDeletion,这里面做什么呢?
  • 遍历子树
    • 因为删除的一个节点,虽然它可能是一个dom节点(在react中是fiber对象)
    • 但对于react组件树来说,dom 节点(fiber对象)下面是可以存放 ClassComponent 这样的节点的
    • 我要删除这个dom节点的同时,相当于也要删除了这个 ClassComponent
    • 这个 ClassComponent 如果有生命周期方法,比如说 componentWillUnmount 这种方法
    • 那么我们要去提醒它,要去调用这个方法, 如何去知道有没有呢?我们就需要去遍历子树中的每一个节点
    • 同样的还有对于像 portal 这种方法,我们要从它的 container 里面去把它相关的 dom 节点去删除
    • 这也是我们要遍历子树的一个原因,所以这个过程是无法避免的
    • 遍历子树需要递归的过程
  • 卸载 ref
    • 因为我们这个doomm上面如果挂载了ref这个属性
    • 那么我们在render这个dom节点它的 owner 上面
    • 比如说 ClassComponent上面, 某个 ref 属性是指向这个dom节点
    • 如果已经把这个dom节点删掉了, 这个ref如果还指向这个dom节点,肯定是不对的
    • 这个时候, 要卸载这个ref
  • 若有组件,需调用它的 componentWillUnmount 的生命周期方法

2 )源码

定位到 packages/react-reconciler/src/ReactFiberCommitWork.js#L1021

查看 commitDeletion

function commitDeletion(current: Fiber): void {
  // dom 环境 默认 true
  if (supportsMutation) {
    // Recursively delete all host nodes from the parent.
    // Detach refs and call componentWillUnmount() on the whole subtree.
    unmountHostComponents(current);
  } else {
    // Detach refs and call componentWillUnmount() on the whole subtree.
    commitNestedUnmounts(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) {
        if (!currentParentIsValid) {
          let parent = node.return;
          // 进入while循环
          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.',
            );
            // 如果它们是这 3 个之一,它们就会跳出这个 while 循环
            switch (parent.tag) {
              case HostComponent:
                currentParent = parent.stateNode;
                currentParentIsContainer = false;
                break findParent; // 注意,break的是上面对应的while循环,而非当前 switch, 下同如此
              case HostRoot:
                currentParent = parent.stateNode.containerInfo;
                currentParentIsContainer = true;
                break findParent;
              case HostPortal:
                currentParent = parent.stateNode.containerInfo;
                currentParentIsContainer = true;
                break findParent;
            }
            // 没有符合条件的,向上去找
            parent = parent.return;
          }
          // 跳出这个while循环之后,他就会设置 parentparentisvalid 为 true
          currentParentIsValid = true;
        }
    
        if (node.tag === HostComponent || node.tag === HostText) {
          commitNestedUnmounts(node);
          // After all the children have unmounted, it is now safe to remove the
          // node from the tree.
          // 上面操作的 currentParentIsContainer 变量,执行不同的 remove 方法,确定从哪里删掉
          if (currentParentIsContainer) {
            removeChildFromContainer((currentParent: any), node.stateNode); // 从container中删除
          } else {
            removeChild((currentParent: any), node.stateNode); // 从父节点中删除
          }
          // Don't visit children because we already visited them.
        } else if (node.tag === HostPortal) {
          // 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; // 找到 child
          }
        } else {
          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; // 找兄弟节点
      }
    }
    
    • 进入 commitUnmount
      // User-originating errors (lifecycles and refs) should not interrupt
      // deletion, so don't let them throw. Host-originating errors should
      // interrupt deletion, so it's okay
      function commitUnmount(current: Fiber): void {
        onCommitUnmount(current);
      
        switch (current.tag) {
          case FunctionComponent:
          case ForwardRef:
          case MemoComponent:
          case SimpleMemoComponent: {
            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 !== null) {
                    safelyCallDestroy(current, destroy);
                  }
                  effect = effect.next;
                } while (effect !== firstEffect);
              }
            }
            break;
          }
          case ClassComponent: {
            // 这里是卸载 ref 的操作
            // 因为Ref是可以作用在classcomponent上面的
            // classcomponent,具有instance而不像function component 没有 instance
            safelyDetachRef(current);
            const instance = current.stateNode;
      
            // 然后需要调用它的 componentWillUnmount 这个方法
            if (typeof instance.componentWillUnmount === 'function') {
              safelyCallComponentWillUnmount(current, instance);
            }
            return;
          }
          case HostComponent: {
            safelyDetachRef(current); // 只卸载 ref
            return;
          }
          case HostPortal: {
            // TODO: this is recursive.
            // We are also not using this parent because
            // the portal will get pushed immediately.
            if (supportsMutation) {
              unmountHostComponents(current); // 注意,这里走了一个递归,也就是调用上级函数,对应上面的 commitNestedUnmounts
            } else if (supportsPersistence) {
              emptyPortalContainer(current);
            }
            return;
          }
        }
      }
      
    • 进入 commitNestedUnmounts
      // 要调用这个方法,说明我们遇到了一个 HostComponent 节点或 HostText 节点,主要是针对 HostComponent
      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
        // 在这个过程中如果找到了有 HostPortal,也对它执行这个方法
        // 它又会去调用我们刚才的那个方法,这就是一个嵌套的递归调用的一个过程
        // 最终目的是要把整个子树给它遍历完成
        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)
          ) {
            node.child.return = node;
            node = node.child;
            continue;
          }
          if (node === root) {
            return;
          }
          // node 一定是 root 节点的子树, 向上找含有兄弟节点的节点
          while (node.sibling === null) {
            if (node.return === null || node.return === root) {
              return;
            }
            node = node.return;
          }
          // 找它的 sibling 兄弟节点,继续执行 while 循环
          node.sibling.return = node.return;
          node = node.sibling;
        }
      }
      
      • 上面的代码完美阐述了删除中间的某个节点,如何处理其子节点的过程,包含 portal 的处理
  • commitDeletion 描述了整个删除的流程

  • 最重要的就是理解这个算法它如何进行递归的调用来遍历整棵子树每一个节点的过程

  • 对于 Portal,ClassComponent,还有 HostComponent,会有不同的操作

  • 需要注意的是,对于HostComponent的子树的遍历会放到这个 commitNestedUnmounts 方法里面去做

  • 对于这个 unmountHostComponents 方法,它遍历的过程的目的是

    • 找到所有的 HostComponent 来调用这个 commitNestedUnmounts 方法
    • 对于 Portal 和 ClassComponent,它们都会去找自己的 child 的节点
    • 而只有对于 HostCommonent,它才会调用嵌套的递归的方法来遍历它的子树
  • 对于这个整体流程,用下面的图来看下,比如说,要删除图中 App下的 div 节点

第一种场景

  • 对这个节点调用了 commitUnmount 方法
  • 然后去找它的child就是Input, 同样也调用 commitUnmount 这个方法
  • 它符合 if ( node.child !== null && (!supportsMutation || node.tag !== HostPortal) ) 这个条件,并 continue
  • 继续向下找,找到 input, 同样调用 commitUnmount 这个方法, 这时候,node.child === null, node !== root, 于是会执行
    while (node.sibling === null) {
      if (node.return === null || node.return === root) {
        return;
      }
      node = node.return;
    }
    // 找它的 sibling 兄弟节点,继续执行 while 循环
    node.sibling.return = node.return;
    node = node.sibling;
    
  • 这时候要找 input的兄弟节点,没有兄弟节点,符合 while (node.sibling === null)
  • 这时候执行这个 while, 向上查找到 Input, 发现 Input是有兄弟节点的,不符合 while (node.sibling === null),跳出
  • 这时候,node 就是 List (Input的兄弟节点),对 List 节点执行 commitUnmount 方法,继续执行
  • if ( node.child !== null && (!supportsMutation || node.tag !== HostPortal) ) 这里
  • List的child存在,并且List不是HostPortal, 这时候就向下去查找,就到了第一个 span 节点
  • 这时候,span节点没有child, 就会找它的sibling, 找到button,发现没有兄弟节点了,就找它的return
  • 最后一个button的return是 List, 而List又是当前循环的root, 这时候,整个方法,内外循环都停止了
  • 这个过程,我们把每个节点都遍历到了,对每个节点都执行了 commitUnmount 方法

第二种场景

  • 与第一种场景不同,这里的第一个span变成了 Portal,其下有一个 div
  • 前一部分与第一种场景类似,当遍历到 Portal 时,调用 commitUnmount 方法,进入其内部
  • 在 switch case 中匹配到了 HostPortal,调用了 unmountHostComponents 方法,并进入其内部
  • 在 else if 中匹配到了 HostPortal,存在child, 找到其child, 也就是 div 节点,继续内部循环
  • 匹配到了 HostComponent, 需要调用 commitNestedUnmounts, 这个div只有一个节点,执行完成后
  • 接着调用下面的 removeChildFromContainer 方法,因为对于 Portal来说,currentParentIsContainer 是 true
  • 接着往下执行到 while 里面的 if (node.return === null || node.return === root),它的 return 是root,由此返回结束循环
  • 返回到 调用的 commitUnmount 里面, 看到 case HostPortal 最后是return, 也就是这个方法结束了
  • 返回到 commitNestedUnmounts 的 while true 里面的 commitUnmount 下面的代码继续执行,会跳过2个if
  • 直接进入 while, 这时候会找 Portal 节点的sibling, 也就是 span, 接着重复场景1向上返回,最终返回到App之下的这个div

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1413401.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SAP包的操作-修改程序所在的包

SAP包的操作 目录 SAP包的操作SAP GUI转换程序所在的包Eclipse转换程序所在的包 SAP GUI转换程序所在的包 Eclipse转换程序所在的包

YOLOv8优化策略:分层特征融合策略MSBlock | YOLO-MS ,超越YOLOv8与RTMDet,即插即用打破性能瓶颈

🚀🚀🚀本文改进:分层特征融合策略MSBlock,即插即用打破性能瓶颈 🚀🚀🚀在YOLOv8中如何使用 1)作为MSBlock使用;2)与c2f结合使用; 🚀🚀🚀YOLOv8-seg创新专栏:http://t.csdnimg.cn/KLSdv 学姐带你学习YOLOv8,从入门到创新,轻轻松松搞定科研; 1)手把…

Docker数据卷挂载(以容器化Mysql为例)

数据卷 数据卷是一个虚拟目录,是容器内目录与****之间映射的桥梁 在执行docker run命令时,使用**-v 本地目录:容器目录**可以完成本地目录挂载 eg.Mysql容器的数据挂载 1.在根目录root下创建目录mysql及三个子目录: cd ~ pwd m…

SpringMVC 环境搭建入门

SpringMVC 是一种基于 Java 的实现 MVC 设计模型的请求驱动类型的轻量级 Web 框架,属于SpringFrameWork 的后续产品,已经融合在 Spring Web Flow 中。 SpringMVC 已经成为目前最主流的MVC框架之一,并且随着Spring3.0 的发布,全面…

2024年材料、控制工程与制造技术国际学术会议(ICMCEMT 2024)

2024年材料、控制工程与制造技术国际学术会议(ICMCEMT 2024) 2024 International Conference on Materials, Control Engineering, and Manufacturing Technology (ICMCEMT 2024) 会议简介: 2024年材料、控制工程与制造技术国际学术会议(ICMCEMT 2024)定于2024年在…

LiveGBS流媒体平台GB/T28181常见问题-如何快速查看推流上来的摄像头并停止摄像头推流?

LiveGBS流媒体平台GB/T28181常见问题-如何快速查看推流上来的摄像头并停止摄像头推流? 1、负载信息2、负载信息说明3、会话列表查看3.1、会话列表 4、停止会话5、搭建GB28181视频直播平台 1、负载信息 实时展示直播、回放、播放、录像、H265、级联等使用数目 2、负…

AR眼镜_ar智能眼镜显示方案|光学方案

AR眼镜是一种智能眼镜,能够将虚拟现实和现实世界相结合,使人们能够在日常生活中体验和参与虚拟现实。然而,AR智能眼镜的制造成本高,开发周期长。要实现AR眼镜的各项功能,需要良好的硬件条件,而AR智能眼镜的…

HCIP-IPV6实验

实验拓扑 实验需求 全网可达 实验思路 配置IP地址 配置路由协议-ospf 配置R2 配置IPV6 配置R2Tunnel 将所有地址引流到Tunnel0/0/0接口 ripng配置 汇总 实验步骤 配置IP地址 以R2为例 [Huawei]sys r2 [r2]int g0/0/0 [r2-GigabitEthernet0/0/0]ip address 12.1.1…

光耦驱动继电器电路图大全

光耦驱动继电器电路图(一) 注: 1U1-1脚可接12V,也可接5V,1U1导通,1Q1导通,1Q1-30V,线圈两端电压为11.7V. 1U1-1脚不接或接地,1U1不通,1Q1截止,1…

安全基础~通用漏洞1

文章目录 知识补充Acess数据库注入MySQL数据库PostgreSQL-高权限读写注入MSSQL-sa高权限读写执行注入Oracle 注入Mongodb 注入sqlmap基础命令 知识补充 order by的意义: union 操作符用于合并两个或多个 select语句的结果集。 union 内部的每个 select 语句必须拥有…

WSL2+ubuntu 18+VsCode 配置C/C++开发环境 踩坑

1. 管理员模式打开cmd,或PowerShell ,输入 wsl --install 可能出现的错误:无法解析服务器名称或地址 解决方式:科学上网 安装WSL时遇到“无法解析服务器名称或地址”的错误及解决方法 - 知乎 错误2:Error 0x8037…

算力、应用、方案,联想布局全栈AI,以自身制造与供应链范本助力千行百业智能化转型升级

1月23日-24日,联想集团举办主题为“算领AI时代 筑基智能变革”的擎智媒体沙龙和新IT思享会“走进联想”活动。在活动中,联想集团副总裁、中国区首席市场官王传东表示,今年是联想成立40周年,联想已构建了全栈智能布局,将…

多个同标题Excel合并为一个

任务是合并6个excel表格,标题如图 方案: S1.将要合并的Excel都放到一个文件夹中 S2.在同文件夹建立一个新的Excel文件,打开 S3.数据-获取数据-来自文件-从文件夹 S4.选好文件夹,点击“合并并转换数据” S5. 确定 S6.没有问题就…

黑群晖屏蔽更新

黑群晖屏蔽更新 修改Host删除控制面板的红点和更新提示 修改Host ssh连接群晖后执行以下命令 sudo vim /etc/hosts按i键进入编辑模式 光标移动定位到最后一行后追加以下两行 127.0.0.1 update.synology.com 127.0.0.1 update7.synology.com按esc键,然后输入:wq并…

基于LLaMA-Factory的微调记录

文章目录 数据模型准备基于网页的简单微调基于网页的简单评测基于网页的简单聊天 LLaMA-Factory是一个非常好用的无代码微调框架,不管是在模型、微调方式还是参数设置上都提供了非常完备的支持,下面是对微调全过程的一个记录。 数据模型准备 微调时一般…

【遥感专题系列】影像信息提取之——基于专家知识的决策树分类

可以将多源数据用于影像分类当中,这就是专家知识的决策树分类器,本专题以ENVI中Decision Tree为例来叙述这一分类器。 本专题包括以下内容: 专家知识分类器概述知识(规则)定义ENVI中Decision Tree的使用 概述 基于知…

Linux的文件系统、软硬链接、动静态库

前要:本次我想给您带来关于 IO 和文件的知识,而文件在本系列中分为内存上的文件和磁盘上的文件。 1.文件概念 1.1.文件读写 在谈及系统接口之前,我们先来从 C 语言的角度来谈及一些前要知识,以辅助我们后续来理解系统 IO。 我们…

JRT执行SQL脚本

我们可能会从其他库导入表到JRT要支持的库里。或者用其他语言生成导表SQL来让JRT执行SQL创建表和导入数据,为此需要一个能运行SQL脚本文件的脚步。 脚本示例,这个是用JRT的M生成迁移表SQL生成的: 导入SQL的脚本: import JRTBL…

详细分析Java的树形工具类(含注释)

目录 前言1. 基本框架2. 实战应用 前言 对应的每个子孙属于该父亲,这其实是数据结构的基础知识,那怎么划分怎么归属呢 对应的基本知识推荐如下: 【数据结构】树和二叉树详细分析(全)【数据结构】B树和B树的笔记详细…

BLIP-2: 基于冻结图像编码器和大型语言模型的语言-图像预训练引导

BLIP-2: 基于冻结图像编码器和大型语言模型的语言-图像预训练引导 项目地址BLIP-2的背景与意义BLIP-2的安装与演示BLIP-2模型库图像到文本生成示例特征提取示例图像-文本匹配示例性能评估与训练引用BLIP-2Hugging Face集成 在语言-图像预训练领域,BLIP-2的出现标志着…