浅谈虚拟DOM、Diff算法、Key机制

news2024/11/28 9:34:57

您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

虚拟DOM

我们都知道虚拟DOM带来的好处,多次更新数据层的数据,最后异步处理只进行一次页面重绘,而这中间的奥秘就是虚拟DOM。在传统的Web应用中,我们往往会把数据的变化实时地更新到用户界面中,就这样,每一次改动都引发了页面的重渲染,虚拟DOM的目的就是将这过程中所有的改动都聚集在一起,计算出所有的变化,统一更新一次DOM。

如果有这样的一段模板代码:

<div class="title">
      <span>你好</span>
      <ul>
        <li>吃饭</li>
        <li>睡觉</li>
      </ul>
</div>

在React中会变转换成虚拟DOM,其实就是一个JS对象:

const vDom = {
  type: 'div',
  props: { class: 'title' },
  children: [
    {
      type: 'span',
      children: '你好'
    },
    {
      type: 'ul',
      children: [
        { type: 'li', children: '吃饭' },
        { type: 'li', children: '睡觉' }
      ]
    }
  ]
}

当我们需要创建或更新视图层时,React会先通过模板创建出这样的虚拟DOM对象,最后再将该对象转换为真实DOM(唯一一次视图更新),当然,在实际React虚拟DOM中是不止这些基本类型的,React会通过React.createElement的方式将整个模板转换成虚拟DOM对象,案例最终生成的虚拟DOM对象如下结构:

image.png

  • typeof:元素类型,可以是原生元素(span/ul/div)、文本或React组件(ReactComponent);

  • key:每个元素的唯一标识,用于Diff算法比较页面元素更新前后差异,下一节会讲到;

  • ref:访问原生DOM节点,在React中用于特殊情况手动操作DOM;

-  props:元素传参,如类名、innerHTML,也可以是一个新的元素(嵌套元素),或者是React组件;

-  owner:当前正在构建的节点所属于哪个React组件;

-  self:指定位于哪个组件实例(非生产环境);

-  _source:元素代码来自于哪个文件以及代码行数,相当于sourcemap(非生产环境);

因此传说中的虚拟DOM本身其实并不稀奇,只是一个JS对象,里面嵌套了许多JS对象,就像是一个树节点,因此在React中,组件模板必须只能有一个根元素,这样就是一个唯一父节点向下向内扩展的数据结构,如果有多个根节点就会变得情况非常复杂,不利于框架本身维护。

如果页面结构复杂再去通过React.createElement的方式去转换虚拟DOM对象,对于开发者而言会变得非常复杂且消耗很多的心智,因此React官方推出了JSX语法,JSX就是JS的扩展,让JavaScript语言获得了模板化,你只需要像这样去写模板,React会通过JSX Babel插件来转换为React.createElement,因此JSX也是其语法糖形式。

const Index = () => {
    return (
        <div>
            你好
            <p>欢迎</p>
            <Children>这是子组件</Children>
        </div>
    )
}

上面的React JSX模板会被Babel解析成React.createElement版本的形式:

const Index = () => {
    return (
        React.createElement(
            'div',
            null,
            '你好',
            React.createElement('p', null, '欢迎'),
            React.createElement('Children', null. '这是子组件')
        )
    )
}

可以感受到,通过JSX语法来代替React.createElement写法大大降低了开发者框架层面带来的负担,可以专注于业务本身。同时提一嘴,为什么React中的组件必须要大写呢?由于要通过Babel进行转换,需要一个条件来判断该节点为原生标签还是React组件,而React也是通过首字母大写来判定节点为元素还是React组件。

Diff算法

React多次更新只进行一次重绘的奥妙——虚拟DOM,那么React是如何进行更新前后的对比,最后进行唯一一次的更新的呢?React会维护两个虚拟DOM树,那么是如何来对比、判断,做出最优的解法呢?这就用到了Diff算法。

在React中,最值得夸赞的就是虚拟DOMDiff算法的结合,Diff算法并非首创,在Vue中也有Diff算法的概念,React引入Diff算法的基础上做了质的优化,如果在计算一棵树到另一棵树有哪些节点发生变化时,传统Diff算法会通过循环+递归的形式去依次对比,并且该过程不可逆,时间复杂度达到了O(n^3),简单理解,如果有一千个节点,就需要计算十亿次。

而React的Diff算法的时间复杂度只有O(n),如果有一千个节点,只需要计算一千次,这是非常巨大的优化,而这优化的过程主要通过了三种策略,分别对应:Tree DiffComponent DiffElement Diff

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计;

  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构;

  3. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分;

按顺序讲解下,首先是Tree Diff:同级比较,既然DOM节点跨层级的移动少到可以忽略不计,React通过了update Depth对虚拟DOM进行层级控制,在同一层如果发现某个子节点不在了,会完全删除并中断向下的比较,这样只遍历一次即可,举个例子:

image.png

如上图所示,在Diff时会一层一层的比较,也就是蓝框的地方,到第二层的时候会发现R节点还在并且属性和节点类型都一致,但是L节点消失了,此时React会直接删除L节点,重新创建。

其次是Component Diff,组件比较,对于React目前策略一共有两种:

  1. 相同类型的组件;

  2. 不同类型的组件;

对于同类型组件,进行常规的虚拟DOM判断即可,但是会有特殊情况,例如当组件A变化到组件B的时候,虚拟DOM没有发生任何变化,所以用户可以通过shouldComponentUpdate来决定是否需要更新和计算。

对于不同类型组件而言,例如组件名不同、组件类型不同(class -> function),React会统一认定为dirty component(脏组件),会直接删除该组件进行重新创建,不做Diff比较。

举个例子:

image.png

当比较D -> G时,虽然两个组件的结构非常相似,但是React判定D为dirty component,就会直接删除D,重新构建G,两个组件不同,结构类似的情况也是非常少的。

最后就是节点对比(Element Diff),对于同一层级的节点进行唯一Key的比较,当所有的节点在同一层级时包含三种操作:插入(INSERT_MARKUP)、移动(MOVE_EXISTING)、删除(REMOVE_NODE)。

  • INSERT_MARKUP:当新的节点不在老的层级中,直接插入到新的层级中。如:C不在A、B中,直接插入;

  • MOVE_EXISTING:在老的层级中有的节点并且在新层级中也有,属性相同、可以复用的情况,采取移动的方式复用以前的DOM节点。如:老层级为A、B、C、D,新层级为A、D、C、B,节点相同,只是位置发生了变化,这种情况只需要复用+移动即可,

  • REMOVE_NODE:老层级中有的节点,新层级中没有的节点,直接删除,或者新旧节点属性发生变化,无法复用,也会执行删除。如:老层级为A、B、C、D,新层级为A、B、C。直接删除D节点。

举例两个实际场景:

场景一:相同节点,移动复用。

image.png

  1. React开始遍历新层级,遇到B节点,判断在旧层级中是否出现,发现了B节点,判断是否要移动B;

  2. 判断一个节点是否移动的条件为index < lastIndexindex为旧层级的下标,lastIndex为新层级中的下标或者移动后的下标,取Math.max(index, lastIndex),对于B,index=1、lastIndex=0,因此不满足移动条件;

  3. 此时到了A的比较,index为0,lastIndex为1,满足index < lastIndex的条件,移动A节点,lastIndex为1;

  4. 相同的方式到了D节点,index为3,lastIndex为1,不满足index < lastIndex的条件,不移动,并且lastIndex更新到3;

  5. 最后到了C节点,index为2,lastIndex为3,满足条件,移动C,lastIndex还是3,此时该层级Element Diff结束;

场景二:有新节点加入、删除节点:

image.png

  1. B与上述例子一样,不移动,lastIndex为1;

  2. 发现没有E节点,直接创建,此时lastIndex还是1;

  3. 到C节点,index=2lastIndex=1,不满足index < lastIndex的条件,不移动C,lastIndex更新到2;

  4. 到A节点,index=0lastIndex=2index < lastIndex,移动A节点;

  5. 不存在D节点,删除D节点;

再来看一下这种特殊情况:

image.png

走到D节点,index=3lastIndex=0,移动D节点,这样lastIndex为3,后续节点都满足移动条件,会影响性能,因此开发中要减少尾部节点移动到首部的操作。

Key

还有一个热门话题,为什么遍历时不要用index作为节点的key?举个例子:

image.png

当循环时,假设有一个长度为100的数组,如果在index为10的时候破坏了后续数组的排列,比如插入或删除了一个节点,长度变成了99或101,这样后续的节点index全部发生了变化,就像这段代码一样:

const Index = () => {
    const [list, setList] = useState(new Array(100).map((item, index) => index));
    
    const change = () => {
        setList((old) => {
            old.splice(10, 1, 'new');
            return [...old];
        })
    }
    
    return (
        <>
            {
                list.map((_, index) => {
                    return <div key={index}>{index}</div>
                })
            }
        </>
        <button onClick={change}>改变</button>
    )
}

对于避免这样大量的性能消耗,你应该将key改为每个遍历节点本身的值,换句话说,上一轮渲染和下一轮渲染中,因为改变某个节点而被依赖改变的节点给定一个唯一标识,避免它被依赖重建。

实际上都是没有变化过的节点,因此key在某一次遍历中或是对于整个数组所有数据变化(分页)时是可用的,其余情况都应该用唯一标识来作为key。如果数组的长度为一万,性能损耗不堪设想。

结尾

如果读完这篇文章让你对于React原理有了更深一步的理解,欢迎留言讨论、点赞收藏。

如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

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

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

相关文章

论文 | 一分钟快速找到自己研究领域的核心期刊

进入知网官网 https://www.cnki.net/ 点击搜索框右边的&#xff1a;出版物检索 鼠标放到顶部的 出版来源导航 旁边的倒三角上 选择期刊导航&#xff1a; 点击核心期刊导航 找到自己感兴趣的领域 点进去就可以看到该期刊中发表的论文&#xff1a;

postgresql regular lock常规锁 烤的内嫩外焦,入口即化

​专栏内容&#xff1a; postgresql内核源码分析 手写数据库toadb 并发编程 个人主页&#xff1a;我的主页 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物. 介绍 常规锁&#xff0c;主要用于数据库对象的加锁&#xff0c…

学Python还是学JAVA,千万别听机构瞎吹!

机构真的为了割韭菜&#xff0c;无所不用其极&#xff0c;过份夸大Python语言的能力或者贬低JAVA。 导致大家要么跟风被割韭菜&#xff0c;学完也用不到。 导致这一主要原因&#xff1a; 1.你不懂行业内需求。 2.你缺乏认知清楚自己的发展规划路线的途径。3.对于编程的优缺点…

Pandas理论与实战(二)

本文章续接上篇文章 目录 1.数据抽取 1.1 抽取一行数据 1.2 抽取多行数据 1.3 抽取指定列数据 1.4 抽取指定行、列数据 1.5 按指定条件抽取数据 2、数据的增加、删除和修改 2.1 数据增加 2.2 修改数据 2.3 删除数据 1.数据抽取 数据分析过程中&#xff0c;并不是所有的数…

CentOS 6 手动将OpenSSH升级到9.3

前言 收到通知说服务器组件存在漏洞 服务器版本:CentOS-6.8-x86_64 目前SSH版本:OpenSSH_5.3p1, OpenSSL 1.0.1e-fips 11 Feb 2013 前置操作 为了避免升级过程中出现的意外导致服务器无法进行连接,建议对重要的内容先进行备份 创建快照 在主机服务商那里为主机创建快照,防止最糟…

Android Studio无法打开问题解决记录

目录 1 问题起因2 发现问题3 解决问题 1 问题起因 问题的起因是我为了运行一个Kotlin项目&#xff0c;但是报了一个错误&#xff1a; Kotlin报错The binary version of its metadata is 1.5.1, expected version is 1.1.16 然后我就上百度去搜了以下&#xff0c;一篇博客让禁用…

GEE:多元线性回归

作者&#xff1a;CSDN _养乐多_ 本文记录了在NDVI、EVI和LAI作为自变量&#xff0c;precipitation作为因变量的条件下&#xff0c;使用linearRegression函数进行线性回归分析的代码&#xff0c;代码在Google Earth Engine&#xff08;GEE&#xff09;平台上实现。具体而言&am…

AI大数据智能视频融合平台EasyCVR新增Ehome黑白名单配置

EasyCVR视频融合平台基于云边端智能协同架构&#xff0c;具有强大的数据接入、处理及分发能力&#xff0c;平台支持海量视频汇聚管理&#xff0c;可支持多协议接入&#xff0c;包括市场主流标准协议与厂家私有协议及SDK&#xff0c;如&#xff1a;国标GB28181、RTMP、RTSP/Onvi…

Linux 学习记录50(QT篇)

Linux 学习记录50(QT篇) 本文目录 Linux 学习记录50(QT篇)一、基于QT的TCP客户端连接1. 代码实现2. 自制的客户端/服务器 二、QT数据库SQL1. QT将数据库分为三个层次2. 实现数据库操作的相关方法 思维导图练习1.2.3.4. 一、基于QT的TCP客户端连接 所需头文件 <QTcpSocket&g…

3G理论概述

文章目录 UMTS&#xff08;通用移动通信系统&#xff0c;Universal Mobile Telecommunications System&#xff09;UTRAN&#xff08;UMTS陆地无线接入网&#xff0c;UMTS Terrestrial Radio Access Network&#xff09;RNC&#xff08;无线网络控制器&#xff0c;Radio Networ…

music21 层级解析(了解次结构方可将任意曲谱与mid互相转换)

这段代码创建了一个音乐乐谱并将其保存为 MIDI 文件&#xff0c;其中包含一个乐器和多个小节。每个小节中包含四个音符或和弦&#xff0c;然后将小节添加到乐谱中。最后&#xff0c;将乐谱写入 MIDI 文件。 首先&#xff0c;通过导入 music21 库来使用它的功能。 import music2…

onvif库封装及qt工程调用onvif库实现设备搜索、获取码流地址等功能

一、前言&#xff1a; 本篇的OnvifManager工程是在vs2010下进行开发编译&#xff0c;它实现了对onvif库的封装调用&#xff0c;目前工程接口实现了对onvif的搜索、码流地址获取、设备重启接口&#xff0c;其他接口后续可以通过更改工程代码进行添加。qt工程myonvif是对OnvifMan…

Zotero翻译插件“zotero pdf translate”——使用时出现广告的问题的解决办法

一、背景&#xff1a; 在Zotero上安装了“zotero-pdf-translate”插件来辅助翻译。今天忽然发现&#xff0c;在使用CNKI翻译引擎的时候&#xff0c;末尾被加上了广告&#xff1a; (查看名企职位领高薪offer&#xff01;--->智联招聘https***dict.cnki.net/ad.html) 如下&…

【IMX6ULL驱动开发学习】16.睡眠机制_poll机制_fasync异步通知(按键控制LED)

学完了休眠唤醒机制、poll机制、异步通知、定时器、tasklet、工作队列、mmap、input子系统后&#xff0c;该沉淀沉淀了 一、睡眠机制 案例&#xff1a;APP程序读取按键值 - 睡眠机制&#xff08;阻塞或非阻塞&#xff09; 1.等待队列头创建 static DECLARE_WAIT_QUEUE_HEA…

从零实现深度学习框架——带Attentiond的Seq2seq机器翻译

引言 本着“凡我不能创造的,我就不能理解”的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导。 要深入理解深度学习,从零开始创建的经验非常重要,从自己可以理解的角度出发,尽量不使用外部完备的框架前提下,实现我…

【Windows】PowerToys - 微软超强生产力工具

Windows用户必看 - 电脑系统增强工具 下载 下载地址 简介 PowerToys 是一组由 Microsoft 创建和维护的开源实用工具集&#xff0c;旨在为 Windows 操作系统提供额外的功能和增强体验。它是为 Windows 10 及更高版本设计的&#xff0c;可帮助用户提高效率、个性化和定制化他们…

apache 安装配置 基础篇(三) 之 虚拟机

apache虚拟主机类型有两种 1、基于名称的虚拟主机 2、基于地址或IP地址的虚拟主机 基于名称的虚拟主机 ## httpd-vhosts.conf 添加如下代码&#xff0c; ## 同时要在httpd.conf中追加监听89端口 Listen 89 ## 修改apache 文件要 在bin路径下面的cmd 录入 httpd -t 检测 代…

centos7搭建postgresql主从(主备)架构

本篇介绍如何在centos7系统搭建一个postgresql主备集群实现最近的HA&#xff08;高可用&#xff09;架构。后续更高级的HA模式都是基于这个最基本的主备搭建。 节点规划 ip 主机名 用途 192.168.182.4 node1 主库&#xff08;读写&#xff09; 192.168.182.5 node2 备库&#…

大神都在用的4个AI绘画工具,记得收藏

AI绘画工具是一种利用人工智能辅助绘画创作的工具&#xff0c;它可以帮助设计师快速绘制高质量的艺术作品。本文列出了国内外4款免费的AI绘画工具&#xff0c;它们使用起来非常简单&#xff0c;而且创作效果非常不错&#xff0c;一起来看看吧&#xff01; 1.即时灵感 即时灵感…