React面试:谈谈虚拟DOM,Diff算法与Key机制

news2024/11/27 20:31:30

1.虚拟dom

原生的JS DOM操作非常消耗性能,而React把真实原生JS DOM转换成了JavaScript对象。这就是虚拟Dom(Virtual Dom)

每次数据更新后,重新计算虚拟Dom,并和上一次生成的虚拟dom进行对比,对发生变化的部分作批量更新。在此其中,React提供了componentShouldUpdate生命周期来让开发者手动控制减少数据变化后不必要的虚拟dom对比,提升性能和渲染效率。

原生html元素代码:

<div class="title">
      <span>Hello ConardLi</span>
      <ul>
        <li>苹果</li>
        <li>橘子</li>
      </ul>
</div>

在React可能存储为这样的JS代码:

const VitrualDom = {
  type: 'div',
  props: { class: 'title' },
  children: [
    {
      type: 'span',
      children: 'Hello ConardLi'
    },
    {
      type: 'ul',
      children: [
        { type: 'li', children: '苹果' },
        { type: 'li', children: '橘子' }
      ]
    }
  ]
}

当我们需要创建或更新元素时,React首先会让这个VitrualDom对象进行创建和更改,然后再将VitrualDom对象渲染成真实DOM;

当我们需要对DOM进行事件监听时,首先对VitrualDom进行事件监听,VitrualDom会代理原生的DOM事件从而做出响应。

虚拟DOM的组成:

通过JSX或React.createElement,React.createClass等方式创建虚拟元素和组件。即ReactElementelement对象,我们的组件最终会被渲染成下面的结构:

  • type:元素的类型,可以是原生html类型(字符串),或者自定义组件(函数或class)
  • key:组件的唯一标识,用于Diff算法,下面会详细介绍
  • ref:用于访问原生dom节点
  • props:传入组件的props,chidren是props中的一个属性,它存储了当前组件的孩子节点,可以是数组(多个孩子节点)或对象(只有一个孩子节点)
  • owner:当前正在构建的Component所属的Component
  • self:(非生产环境)指定当前位于哪个组件实例
  • _source:(非生产环境)指定调试代码来自的文件(fileName)和代码行数(lineNumber)
<div className="title">
      <span>Hello ConardLi</span>
      <ul>
        <li>苹果</li>
        <li>橘子</li>
      </ul>
</div>

将此JSX元素打印出来,证实虚拟DOM本质就是js对象: 在这里插入图片描述

其中,在jsx中使用的原生元素标签,其type为标签名。而如果是函数组件或class组件,其type就是对应的class或function对象参考 前端进阶面试题详细解答

在这里插入图片描述 在这里插入图片描述

2.diff算法

React需要同时维护两棵虚拟DOM树:一棵表示当前的DOM结构,另一棵在React状态变更将要重新渲染时生成。React通过比较这两棵树的差异,决定是否需要修改DOM结构,以及如何修改。这种算法称作Diff算法。

这个算法问题有一些通用的解决方案,即生成将一棵树转换成另一棵树的最小操作数。 然而,即使在最前沿的算法中,该算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量。

如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。于是 React 在以下两个假设的基础之上提出了一套 O(n) 的启发式算法:

1:两个不同类型的元素会产生出不同的树;

2:开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定;

React diff算法大致执行过程:

Diff算法会对新旧两棵树做深度优先遍历,避免对两棵树做完全比较,因此算法复杂度可以达到O(n)。然后给每个节点生成一个唯一的标志:

在这里插入图片描述 在遍历的过程中,每遍历到一个节点,就将新旧两棵树作比较,并且只对同一级别的元素进行比较:

在这里插入图片描述 也就是只比较图中用虚线连接起来的部分,把前后差异记录下来。

React diff算法具体策略:

(1)tree diff

tree diff主要针对的是React dom节点跨层级的操作。由于跨层级的DOM移动操作较少,所以React diff算法的tree diff没有针对此种操作进行深入比较,只是简单进行了删除和创建操作

在这里插入图片描述 如图所示,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单地考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。

当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,diff 的执行情况:create A → create B → create C → delete A

由此可以发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的整个树被重新创建。这是一种影响 React 性能的操作,因此官方建议不要进行 DOM 节点跨层级的操作。

基于上述原因,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真正地移除或添加 DOM 节点

(2)component diff:

component diff是专门针对更新前后的同一层级间的React组件比较的diff 算法:

  • 如果是同一类型的组件,按照原策略继续比较 Virtual DOM 树(例如继续比较组件props和组件里的子节点及其属性)即可。
  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点,即销毁原组件,创建新组件。
  • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切知道这点,那么就可以节省大量的 diff 运算时间。因此,React 允许用户通过 shouldComponentUpdate()来判断该组件是否需要进行 diff 算法分析

在这里插入图片描述

如图 所示,当组件 D 变为组件 G 时,即使这两个组件结构相似,一旦 React 判断 D 和G 是不同类型的组件,就不会比较二者的结构,而是直接删除组件 D,重新创建组件 G 及其子节点。

虽然当两个组件是不同类型但结构相似时,diff 会影响性能,但正如 React 官方博客所言:不同类型的组件很少存在相似 DOM树的情况,因此这种极端因素很难在实际开发过程中造成重大的影响

(3)element diff

element diff是专门针对同一层级的所有节点(包括元素节点和组件节点)的diff算法。当节点处于同一层级时,diff 提供了 3 种节点操作,分别为 INSERT_MARKUP(插入)MOVE_EXISTING(移动)REMOVE_NODE(删除)

我们将虚拟dom树中欲比较的某同一层级的所有节点的集合分别称为新集合和旧集合,则有以下策略:

  • INSERT_MARKUP:新集合的某个类型组件或元素节点不存在旧集合里,即全新的节点,需要对新节点执行插入操作。
  • MOVE_EXISTING:新集合的某个类型组件或元素节点存在旧集合里,且 element 是可更新的类型,generateComponent-Children 已调用receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
  • REMOVE_NODE:旧集合的某个组件或节点类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者旧组件或节点不在新集合里的,也需要执行删除操作。

在这里插入图片描述 如图 所示,旧集合中包含节点A、B、C 和 D,更新后的新集合中包含节点 B、A、D 和C(只是发生了位置变化,各自节点以及内部数据没有变化),此时新旧集合按顺序进行逐一的diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除旧集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。

React 发现这类操作烦琐冗余,因为这些都是相同的节点,但由于位置顺序发生变化,导致需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可。

针对这一现象,React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,。见下面key机制

3. key机制

(1)key的作用

当同一层级的某个节点添加了对于其他同级节点唯一的key属性,当它在当前层级的位置发生了变化后。react diff算法通过新旧节点比较后,如果发现了key值相同的新旧节点,就会执行移动操作(然后依然按原策略深入节点内部的差异对比更新),而不会执行原策略的删除旧节点,创建新节点的操作。这无疑大大提高了React性能和渲染效率

(2)key的具体执行过程

首先,对新集合中的节点进行循环遍历 for (name in nextChildren),通过唯一的 key 判断新旧集合中是否存在相同的节点 if (prevChild === nextChild),如果存在相同节点,则进行移动操作,但在移动前需要将当前节点在旧集合中的位置与 lastIndex 进行比较 if (child._mountIndex < lastIndex),否则不执行该操作。

例子1:同一层级的所有节点只发生了位置变化: 在这里插入图片描述

按新集合中顺序开始遍历

  1. B在新集合中 lastIndex(类似浮标) = 0, 在旧集合中 index = 1,index > lastIndex 就认为 B 对于集合中其他元素位置无影响,不进行移动,之后lastIndex = max(index, lastIndex) = 1
  2. A在旧集合中 index = 0, 此时 lastIndex = 1, 满足 index < lastIndex, 则对A进行移动操作,此时lastIndex = max(Index, lastIndex) = 1
  3. D和B操作相同,同(1),不进行移动,此时lastIndex=max(index, lastIndex) = 3
  4. C和A操作相同,同(2),进行移动,此时lastIndex = max(index, lastIndex) = 3

上述结论中的移动操作即对节点进行更新渲染,而不进行移动则表示无需更新渲染

例子2:同一层级的所有节点发生了节点增删和节点位置变化:

在这里插入图片描述

  1. 同上面那种情形,B不进行移动,lastIndex=1
  2. 新集合中取得E,发现旧中不存在E,在 lastIndex处创建E,lastIndex++
  3. 在旧集合中取到C,C不移动,lastIndex=2
  4. 在旧集合中取到A,A移动到新集合中的位置,lastIndex=2
  5. 完成新集合中所有节点diff后,对旧集合进行循环遍历,寻找新集合中不存在但就集合中的节点(此例中为D),删除D节点。

(3)index作为key

react中常常会用到通过遍历(如Array.map)来在当前层级动态生成多个子节点的操作。这是常见的列表数据渲染场景。

React官方建议不要用遍历的index作为这种场景下的节点的key属性值。比如当前遍历的所有节点类型都相同,其内部文本不同,在用index作key的情况下,当我们对原始的数据list进行了某些元素的顺序改变操作,导致了新旧集合中在进行diff比较时,相同index所对应的新旧的节点其文本不一致了,就会出现一些节点需要更新渲染文本,而如果用了其他稳定的唯一标识符作为key,则只会发生位置顺序变化,无需更新渲染文本,提升了性能

此外使用index作为key很可能会存在一些出人意料的显示错误的问题:

{this.state.data.map((v,index) => <Item key={index} v={v} />)}
// 开始时:['a','b','c']=>
<ul>
    <li key="0">a <input type="text"/></li>
    <li key="1">b <input type="text"/></li>
    <li key="2">c <input type="text"/></li>
</ul>

// 数组重排 -> ['c','b','a'] =>
<ul>
    <li key="0">c <input type="text"/></li>
    <li key="1">b <input type="text"/></li>
    <li key="2">a <input type="text"/></li>
</ul>

上面实例中在数组重新排序后,key对应的实例都没有销毁,而是重新更新。具体更新过程我们拿key=0的元素来说明, 数组重新排序后:

  • 组件重新render得到新的虚拟dom;
  • 新老两个虚拟dom进行diff,新老版的都有key=0的组件,react认为同一个组件,则只可能更新组件;
  • 然后比较其children,发现内容的文本内容不同(由a—>c),而input组件并没有变化,这时触发组件的componentWillReceiveProps方法,从而更新其子组件文本内容;
  • 因为组件的children中input组件没有变化,其又与父组件传入的任props没有关联,所以input组件不会更新(即其componentWillReceiveProps方法不会被执行),导致用户输入的值不会变化。

(4)key机制的缺点

在这里插入图片描述 如图 所示,若新集合的节点更新为 D、A、
B、C,与旧集合相比只有 D 节点移动,而 A、B、C 仍然保持原有的顺序,理论上 diff 应该只需对 D 执行移动操作,然而由于 D 在旧集合中的位置是最大的,导致其他节点的 _mountIndex <lastIndex,造成 D 没有执行移动操作,而是 A、B、C 全部移动到 D 节点后面的现象.

在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作。当节点数量过大或更新操作过于频繁时,这在一定程度上会影响 React 的渲染性能。。

(5)key使用注意事项:

  1. 如果遍历的列表子节是作为纯展示,而不涉及到列表元素顺序的动态变更,那使用index作为key还是没有问题的。
  2. key只是针对同一层级的节点进行了diff比较优化,而跨层级的节点互相之间的key值没有影响
  3. 大部分情况下,通过遍历的同一层级的使用了key属性的元素节点其节点类型是相同的(比如都是span元素或者同一个组件)。如果存在新旧集合中,相同的key值所对应的节点类型不同(比如从span变成div),这相当于完全替换了旧节点,删除了旧节点,创建了新节点。
  4. 如果新集合中,出现了旧集合没有存在过的key值。例如某个节点的key之前为1,现在为100,但旧集合中其他节点也没有使用100这个key值。说明没发生过移动操作,此时diff算法会对对应的节点进行销毁并重新创建。这在一些场景中会比较有用(比如重置某个组件的状态)
  5. key值在比较之前都会被执行toString()操作,所以尽量不要使用object类型的值作为key,会导致同一层级出现key值相同的节点。key值重复的同一类型的节点或组件很可能出现拷贝重复内部子元素的问题

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

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

相关文章

Ansys Zemax | 用于数字投影光学中均匀照明的蝇眼阵列

简介 在数字投影仪设计中&#xff0c;我们希望确保数字光源与投影图像在辐照度分布相匹配。因此&#xff0c;这一约束要求投影仪设计包含均匀照明的空间光调制器——通常以LCD面板的形式呈现。理论上听起来很容易&#xff0c;但实际上&#xff0c;此面板上的光源光束通常是高斯…

语音输入转文字怎么操作?分享几种语音转文字技巧

相信有不少小伙伴在整理语音文件的时候&#xff0c;都会有过怎样把这些语音直接转换成文字的想法吧。每次在我开完会之后&#xff0c;需要对会议语音进行整理时&#xff0c;都会产生这种想法。因为我们需要不断的去听这个会议的语音内容&#xff0c;这样做既费时又费力。但其实…

MATLAB生成2D和3D格网(GUI程序)

目录 一、写函数DataStructure_Fnc 二、控件属性 三、生成2D格网代码 三、生成3D格网代码 一、写函数DataStructure_Fnc 函数代码&#xff0c;生成三角网需要调用此函数 function DataStructureDataStructure_Fnc(Table) [row col]size(Table); Table(1:end,5:7)-1; for j1…

【配置指导】如何配置dataFEED edgeConnector Siemens以实现西门子PLC与阿里云之间的双向通信

本配置指导手册介绍了如何配置dataFEED edgeConnector Siemens&#xff0c;以通过MQTT来将西门子S7-1200 PLC数据上传到阿里云&#xff1b;以及从阿里云发布数据&#xff0c;并传输到PLC中&#xff0c;从而实现西门子S7-1200 PLC与阿里云之间的双向通信。 主要内容包括&#xf…

30-Vue之ECharts-直角坐标系的常用配置

直角坐标系的常用配置前言直角坐标系常用配置网格坐标轴区域缩放前言 本篇来学习下直角坐标系的常用配置 直角坐标系 直角坐标系的图表指的是带有x轴和y轴的图表, 常见的直角坐标系的图表有: 柱状图 折线图 散点图 常用配置 网格 grid&#xff1a;是用来控制直角坐标系的…

可落地的、不基于框架的分布式事务解决方案

两阶段提交 2PC 在MySQL InnoDB中&#xff0c;为了保证Bin Log和Redo Log的一致性&#xff0c;便采用了两阶段提交&#xff1b;ZooKeeper、ETCD集群为了保证数据一致性&#xff0c;也采用了两阶段提交&#xff0c;RocketMQ的事务消息也采用了两阶段提交&#xff0c;可见两阶段…

从VirtualBox换成KVM虚拟机管理程序?

好消息是&#xff0c;您可以轻松地将VDI格式的VirtualBox VM迁移到qcow2(即KVM的磁盘映像格式)&#xff0c;不用创建新的KVM来宾计算机。 我们在本文中将概述如何将VirtualBox VM迁移到Linux中KVM VM的逐步过程。 第一步&#xff1a;列出现有的VirtualBox映像 首先&#xff0c…

泰斯公式Thiem’s equation地下水

基本形式 泰斯公式1描述了在含水层抽水时的地下水流动。 多井作业时非承压含水层的方程形式如下 H(s)和H0(s)分别表示s点的估计地下水位和初始地下水位&#xff0c;K表示水力导率&#xff0c;ri表示预测位置与贡献井i之间的距离&#xff0c;n是贡献井的集合&#xff0c;Q表…

Win11 21H2 12月最新更新了哪些内容?

微软今天发布了12月最新的累积更新补丁&#xff0c;用户可以升级KB5021234将版本号提升至 build 22000.1335&#xff0c;并解决了远程网络问题以及可能影响数据保护应用程序编程接口 &#xff08;DPAPI&#xff09; 解密的问题。此外&#xff0c;该更新还包括之前在 11 月 15 日…

11-FreeRTOS配置函数 FreeRTOSConfig.h

1-FreeRTOSConfig.h介绍 FreeRTOS中的相关定义多数都在FreeRTOSConfig.h中&#xff0c;整个FreeRTOS的定义调用都可以在这里定义&#xff0c;当然你也可以自己命名一个文件实现自定义。 下面是这个文件的内容&#xff0c;如下&#xff1a; #ifndef FREERTOS_CONFIG_H #define…

Graph Neural Networks for Social Recommendation学习笔记

1 目标 学习user embedding和item embedding。 2 框架 3 用户建模 3.1 利用历史记录对用户建模 3.2 利用社交关系对用户建模

10.9.1-Dataway+Echarts动态图表方案

文章目录1. 技术选型2. 实现方案2.1. 方案介绍2.2. 方案实现&#xff08;demo&#xff09;2.2.1. 使用echarts绘制html静态页2.2.1.1. 选择合适的图表2.2.1.2. 下载html demo2.2.2. 使用Dataway准备数据接口2.2.2.1. 部署dataway2.2.2.2. 创建数据接口2.2.3. 调试html demo da…

代码随想录算法训练营第七天| 哈希表理论基础 ,454.四数相加II, 383. 赎金信, 15. 三数之和, 18. 四数之和

代码随想录算法训练营第七天| 哈希表理论基础 &#xff0c;454.四数相加II&#xff0c; 383. 赎金信&#xff0c; 15. 三数之和&#xff0c; 18. 四数之和 454.四数相加II 建议&#xff1a;本题是 使用map 巧妙解决的问题&#xff0c;好好体会一下 哈希法 如何提高程序执行效…

【洞察人性】 理解行为背后的动机

《洞察人性》 关于作者 阿尔弗雷德•阿德勒&#xff0c;奥地利精神病学家&#xff0c; 人本主义心理学先驱&#xff0c;曾经在美国哥伦比亚大学任客座教授。同时他也是个体心理学的创始人&#xff0c;在学术界拥有重要的地位。著作有《自卑与超越》《人性的研究》 《洞察人性…

DPDK源码分析之rte_eal_init

EAL是什么 环境抽象层(EAL)负责获得对底层资源(如硬件和内存空间)的访问。对于应用程序和其他库来说&#xff0c;使用这个通用接口可以不用关系具体操作系统的环境细节。rte_eal_init初始化例程负责决定如何分配操作系统的这些资源(即内存空间、设备、定时器、控制台等等)。 …

【IVIF:搜索架构】

Searching a Hierarchically Aggregated Fusion Architecture for Fast Multi-Modality Image Fusion &#xff08;搜索用于快速多模态图像融合的分层聚合融合架构&#xff09; 现有的基于CNN的方法使主要点在于设计各种体系结构&#xff0c;以端到端的方式实现这些任务。但是…

JSP ssh美食娱乐分享网站系统myeclipse开发oracle数据库MVC模式java编程计算机网页设计

一、源码特点 JSP ssh美食娱乐分享网站系统是一套完善的web设计系统&#xff08;系统采用ssh框架进行设计开发&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采 用B/S模式开发。开发环境为TOMCA…

学习二叉树必须要了解的各种遍历方式及节点统计

哈喽&#xff0c;大家好&#xff0c;我是小林。今天给大家分享一下对二叉树的一些常规操作。 愿我们都能保持一颗向上的心。 目录一、前序遍历二、中序遍历三、后序遍历四、 统计节点个数五、统计叶子节点个数六、第K层的节点个数七、二叉树的深度八、查找值为x的节点九、层序遍…

TensorFlow TFRecords简介

TensorFlow TFRecords简介 这篇博客将介绍TensorFlow的TFRecords&#xff0c;提供有关TFRecords的所有信息的一应俱全的介绍。从如何构建基本TFRecords到用于训练 SRGAN 和 ESRGAN 模型的高级TFRecords的所有内容。包括什么是TFRecords&#xff0c;如何序列化&#xff0c;反序…

SQL 语句练习03

目录 一、建表 二、插入数据 三、查询 一、建表 这里先建好我们下面查询需要的表&#xff0c;方便后续查询。 建立如下学生表(命名格式“姓名拼音_三位学号_week5s”&#xff0c; 如LBJ_023_week5s&#xff09;create table LYL_116_week5s(SNO varchar(4) primary key,SNA…