老生常谈React的diff算法原理-面试版

news2024/10/6 12:29:03

第一次发文章 not only(虽然)版式可能有点烂 but also (但是)最后赋有手稿研究 finally看完他你有收获

diff算法:对于update的组件,他会将当前组件与该组件在上次更新是对应的Fiber节点比较,将比较的结果生成新的Fiber节点。

! 为了防止概念混淆,强调

一个DOM节点,在某一时刻最多会有4个节点和他相关。

- 1.current Fiber。如果该DOM节点已在页面中,current Fiber代表该DOM节点对应的Fiber节点。
- 2.workInProgress Fiber。如果该DOM节点将在本次更新中渲染到页面中,workInProgress Fiber代表该DOM节点对应的Fiber节点。
- 3.DOM节点本身。
- 4.JSX对象。即ClassComponent的render方法的返回结果,或者FunctionComponent的调用结果,JSX对象中包含描述DOM节点信息。
diff算法的本质上是对比1和4,生成2。

Diff的瓶颈以及React如何应对

由于diff操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中
将前后两棵树完全比对的算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量。

如果在React中使用了该算法,那么展示1000个元素所需要执行的计算量将在十亿的量级范围。
这个开销实在是太过高昂。

所以为了降低算法复杂度,React的diff会预设3个限制:

 1.同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。
 2.不同类型的元素会产生出不同的树。如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。
 3.者可以通过 key prop来暗示哪些子元素在不同的渲染下能保持稳定。

那么我们接下来看一下Diff是如何实现的

我们可以从同级的节点数量将Diff分为两类:

 1.当newChild类型为object、numberstring,代表同级只有一个节点
- 2.当newChild类型为Array,同级有多个节点

单节点diff

以类型Object为例,会进入这个函数reconcileSingleElement

参考 前端进阶面试题详细解答

这个函数会做如下事情:

让我们看看第二步判断DOM节点是否可以复用是如何实现的。

从代码可以看出,React通过先判断key是否相同,如果key相同则判断type是否相同,只有都相同时一个DOM节点才能复用。

这里有个细节需要关注下:

1.当child !== null且key相同且type不同时执行deleteRemainingChildren将child及其兄弟fiber都标记删除。

2.当child !== null且key不同时仅将child标记删除。

例子:当前页面有3个li,我们要全部删除,再插入一个p。

由于本次更新时只有一个p,属于单一节点的Diff,会走上面介绍的代码逻辑。

解释:

在reconcileSingleElement中遍历之前的3个fiber(对应的DOM为3个li),寻找本次更新的p是否可以复用之前的3个fiber中某个的DOM。

当key相同且type不同时,代表我们已经找到本次更新的p对应的上次的fiber,但是 p 与 li 的type不同,不能复用。
既然唯一的可能性已经不能复用,则剩下的fiber都没有机会了,所以都需要标记删除。

当key不同时只代表遍历到的该fiber不能被p复用,后面还有兄弟fiber还没有遍历到。所以仅仅标记该fiber删除。

练习题:

习题1: 未设置key prop默认 key = null;,所以更新前后key相同,都为null,但是更新前type为div,更新后为p,type改变则不能复用。

习题2: 更新前后key改变,不需要再判断type,不能复用。

习题3: 更新前后key没变,但是type改变,不能复用。

习题4: 更新前后key与type都未改变,可以复用。children变化,DOM的子元素需要更新。

多节点diff

同级多个节点的Diff,一定属于下面3中情况的一种或多种。

  • 情况1:节点更新

  • 情况2:节点新增或减少

  • 情况3:节点位置变化

注意在这里diff算法无法使用双指针优化
在我们做数组相关的算法题时,经常使用双指针从数组头和尾同时遍历以提高效率,但是这里却不行。
虽然本次更新的JSX对象 newChildren为数组形式,但是和newChildren中每个组件进行比较的是current fiber
同级的Fiber节点是由sibling指针链接形成的单链表。

即 newChildren[0]与fiber比较,newChildren[1]与fiber.sibling比较。
所以无法使用双指针优化。

基于以上原因,Diff算法的整体逻辑会经历两轮遍历:

1.第一轮遍历:处理更新的节点。
2.第二轮遍历:处理剩下的不属于更新的节点

第一轮遍历:

第一轮遍历步骤如下:

let i = 0,遍历newChildren,将newChildren[i]与oldFiber比较,判断DOM节点是否可复用。

如果可复用,i++,继续比较newChildren[i]与oldFiber.sibling,可以复用则继续遍历。

如果不可复用,立即跳出整个遍历,第一轮遍历结束。

如果newChildren遍历完(即i === newChildren.length - 1)或者oldFiber遍历完(即oldFiber.sibling === null)
跳出遍历,第一轮遍历结束。

上面3跳出的遍历

此时newChildren没有遍历完,oldFiber也没有遍历完。

上例子:

2个节点可复用,遍历到key === 2的节点发现type改变,不可复用,跳出遍历。

此时oldFiber剩下key === 2未遍历,newChildren剩下key === 2、key === 3未遍历。

上面4跳出的遍历

可能newChildren遍历完,或oldFiber遍历完,或他们同时遍历完。

上例子:

带着第一轮遍历的结果,我们开始第二轮遍历。

第一轮遍历:(4种情况)

- 1.newChildren与oldFiber同时遍历完 

那就是最理想的情况:只有组件更新。此时Diff结束。
- 2.newChildren没遍历完,oldFiber遍历完

已有的DOM节点都复用了,这时还有新加入的节点,意味着本次更新有新节点插入
我们只需要遍历剩下的newChildren为生成的workInProgress fiber依次标记Placement。
- 3.newChildren遍历完,oldFiber没遍历完

意味着本次更新比之前的节点数量少,有节点被删除了。所以需要遍历剩下的oldFiber,依次标记Deletion。
- 4.newChildren与oldFiber都没遍历完

这意味着有节点在这次更新中改变了位置。

改变了位置就需要我们处理移动的节点

由于有节点改变了位置,所以不能再用位置索引i对比前后的节点,那么如何才能将同一个节点在两次更新中对应上呢?
我们需要使用key。

为了快速的找到key对应的oldFiber,我们将所有还未处理的oldFiber存入以keykey,oldFiber为value的Map中。

接下来遍历剩余的newChildren,通过newChildren[i].key就能在existingChildren中找到key相同的oldFiber

标记节点是否移动

!既然我们的目标是寻找移动的节点,那么我们需要明确:节点是否移动是以什么为参照物?

我们的参照物是:最后一个可复用的节点在oldFiber中的位置索引(用变量lastPlacedIndex表示)。
由于本次更新中节点是按newChildren的顺序排列。
在遍历newChildren过程中,每个遍历到的可复用节点一定是当前遍历到的所有可复用节点中最靠右的那个
即一定在lastPlacedIndex对应的可复用的节点在本次更新中位置的后面。

那么我们只需要比较遍历到的可复用节点在上次更新时是否也在lastPlacedIndex对应的oldFiber后面
就能知道两次更新中这两个节点的相对位置改变没有。

我们用变量oldIndex表示遍历到的可复用节点在oldFiber中的位置索引。

如果oldIndex < lastPlacedIndex,代表本次更新该节点需要向右移动。

lastPlacedIndex初始为0,每遍历一个可复用的节点,如果oldFiber >= lastPlacedIndex,则lastPlacedIndex = oldFiber。

下面通过两个demo来看一下react团队的diff算法精髓

demo1

// 之前 abcd // 之后 acdb

=第一轮遍历开始=

a(之后)vs a(之前)  

key不变,可复用

此时 a 对应的oldFiber(之前的a)在之前的数组(abcd)中索引为0

所以 lastPlacedIndex = 0;

继续第一轮遍历…

c(之后)vs b(之前)  

key改变,不能复用,跳出第一轮遍历

此时 lastPlacedIndex === 0;

=第一轮遍历结束=

=第二轮遍历开始=

newChildren === cdb,没用完,不需要执行删除旧节点

oldFiber === bcd,没用完,不需要执行插入新节点

将剩余oldFiber(bcd)保存为map

// 当前oldFiber:bcd

// 当前newChildren:cdb

继续遍历剩余newChildren

key === c 在 oldFiber中存在

const oldIndex = c(之前).index;

此时 oldIndex === 2;  // 之前节点为 abcd,所以c.index === 2

比较 oldIndex 与 lastPlacedIndex;

如果 oldIndex >= lastPlacedIndex 代表该可复用节点不需要移动

并将 lastPlacedIndex = oldIndex;

如果 oldIndex < lastplacedIndex 该可复用节点之前插入的位置索引小于这次更新需要插入的位置索引,代表该节点需要向右移动

在例子中,oldIndex 2 > lastPlacedIndex 0,

则 lastPlacedIndex = 2;

c节点位置不变

继续遍历剩余newChildren

// 当前oldFiber:bd

// 当前newChildren:db

key === d 在 oldFiber中存在

const oldIndex = d(之前).index;

oldIndex 3 > lastPlacedIndex 2 // 之前节点为 abcd,所以d.index === 3

则 lastPlacedIndex = 3;

d节点位置不变

继续遍历剩余newChildren

// 当前oldFiber:b

// 当前newChildren:b

key === b 在 oldFiber中存在

const oldIndex = b(之前).index;

oldIndex 1 < lastPlacedIndex 3 // 之前节点为 abcd,所以b.index === 1

则 b节点需要向右移动

=第二轮遍历结束=

!最终acd 3个节点都没有移动,b节点被标记为移动

demo2

// 之前 abcd

// 之后 dabc

=第一轮遍历开始=

d(之后)vs a(之前)  
key改变,不能复用,跳出遍历

=第一轮遍历结束=

=第二轮遍历开始=

newChildren === dabc,没用完,不需要执行删除旧节点

oldFiber === abcd,没用完,不需要执行插入新节点

将剩余oldFiber(abcd)保存为map

继续遍历剩余newChildren

// 当前oldFiber:abcd

// 当前newChildren dabc

key === d 在 oldFiber中存在

const oldIndex = d(之前).index;

此时 oldIndex === 3; // 之前节点为 abcd,所以d.index === 3

比较 oldIndex 与 lastPlacedIndex;

oldIndex 3 > lastPlacedIndex 0

则 lastPlacedIndex = 3;

d节点位置不变

继续遍历剩余newChildren

// 当前oldFiber:abc

// 当前newChildren abc

key === a 在 oldFiber中存在

const oldIndex = a(之前).index; // 之前节点为 abcd,所以a.index === 0

此时 oldIndex === 0;

比较 oldIndex 与 lastPlacedIndex;

oldIndex 0 < lastPlacedIndex 3

则 a节点需要向右移动

继续遍历剩余newChildren

// 当前oldFiber:bc

// 当前newChildren bc

key === b 在 oldFiber中存在

const oldIndex = b(之前).index; // 之前节点为 abcd,所以b.index === 1

此时 oldIndex === 1;

比较 oldIndex 与 lastPlacedIndex;

oldIndex 1 < lastPlacedIndex 3

则 b节点需要向右移动

继续遍历剩余newChildren

// 当前oldFiber:c

// 当前newChildren c

key === c 在 oldFiber中存在

const oldIndex = c(之前).index; // 之前节点为 abcd,所以c.index === 2

此时 oldIndex === 2;

比较 oldIndex 与 lastPlacedIndex;

oldIndex 2 < lastPlacedIndex 3

则 c节点需要向右移动

=第二轮遍历结束=

!可以看到,我们以为从 abcd 变为 dabc,只需要将d移动到前面。 !但实际上React保持d不变,将abc分别移动到了d的后面。

用张老生常谈的图

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

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

相关文章

关于2022年国内软件质量调查问卷的一些感悟与收获

&#x1f4cb;前言 1️⃣关于2022年国内软件质量调查主题征文活动 CSDN《2022年国内软件质量调查》正式开启&#xff0c;我们诚邀各位博主&#xff0c;特别是测试领域的各位技术er参与调查&#xff0c;并围绕主题&#xff0c;撰写《我填写“2022年国内软件质量调查问卷”的感想…

刷了一个月面试题,终于拿到了字节跳动的offer

一面 自我介绍项目中的监控&#xff1a;那个监控指标常见的有哪些&#xff1f;微服务涉及到的技术以及需要注意的问题有哪些&#xff1f;注册中心你了解了哪些&#xff1f;consul 的可靠性你了解吗&#xff1f;consul 的机制你有没有具体深入过&#xff1f;有没有和其他的注册…

(二)fiber的基本认识

上一篇文章我们了解了react新老结构的差异以及存在的缺点&#xff0c;其中react的解决方案就是采用fiber架构和添加Schedule模块。 ✍️&#xff1a;Schedule模块的主要工作是任务的调度&#xff0c;负责调度不同优先级任务的执行时机&#xff0c;这个我们后面再讲&#xff0c;…

Ardor公链生态与Jelurida产业区块链布局

Ardor公链 Ardor公链&#xff08;ARDR&#xff09;基于NXT公链&#xff0c;并于2018年1月1日推出了多链架构&#xff0c;旨在克服单链本质上的局限性。根据Ardor平台的白皮书&#xff0c;其主要目标是&#xff1a; 解决最终用户必须拥有作为手续费&#xff08;Gas费&#xff…

FineReport复杂表格软件- 相对层次坐标

1. 概述 相对层次坐标是用来描述目标单元格和当前单元格之间的位置关系的表达式&#xff0c;概念图如下图所示&#xff1a; 说明&#xff1a; 参数 说明 Cellx 表示需要返回结果的单元格 Celly 表示位移时参考的单元格 z 代表相对位移的位置 注&#xff1a;相对后移需要…

【Linux】必须掌握的Linux常见指令分类讲解

目录一.Linux下的文件树二.工作目录切换命令1.ls——显示当前路径下的文件和目录2.pwd——显示当前目录的绝对值路径3.cd——切换至指定目录三.文件目录管理命令1.touch——创建空文件2.tree——树状打印目录3.mkdir——创建目录4.rmdir 和 rm ——删除目录5.cp——拷贝文件或目…

Python编程小白入门技巧,从入门到精通只需一个月。

毫无疑问&#xff0c;Python 是当下最火的编程语言之一。对于许多未曾涉足计算机编程的领域「小白」来说&#xff0c;深入地掌握 Python 看似是一件十分困难的事。其实&#xff0c;只要掌握了科学的学习方法并制定了合理的学习计划&#xff0c;Python 从 入门到精通只需要一个月…

【iOS】接口与API设计

文章目录前言用前缀避免命名空间冲突提供“全能初始化方法”实现description方法尽量使用不可变对象使用清晰而协调的命名方式方法命名类与协议的命名为私有方法名加前缀理解Objective-C错误模型理解NSCopying协议前言 我们在构建程序应用时&#xff0c;如果决定重用代码&…

DPDK 网卡驱动学习

DPDK版本19.02 初始化&#xff1a; /* Launch threads, called at application init(). */ int rte_eal_init(int argc, char **argv) {.../* rte_eal_cpu_init() ->* eal_cpu_core_id()* eal_cpu_socket_id()* 读取/sys/devices/system/[cpu|node]* 设置lcore_con…

考试管理系统

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 模块划分&#xff1a;老师模块、班级模块、学生模块、课程模块、试题模块、试卷模块、 组卷模块、考试模块、答题模块 管…

Python数据分析实战之用户消费行为数据分析

任务1&#xff1a;数据预处理 表格数据资源如下百度网盘&#x1f447; 链接&#xff1a;https://pan.baidu.com/s/1pUYfRIe557v6O9ByB2rhEw 提取码&#xff1a;ovgl import numpy as np import pandas as pd import matplotlib.pyplot as plt # %matplotlib inline # 更改绘…

OrangePi 5 Docker下安装OpenWRT作软路由(同样适用于树莓派等设备)

OrangePi5 Docker下安装OpenWRT作软路由&#xff08;同样适用于树莓派等设备&#xff09; 说明 本文的软路由作为家中的二级路由&#xff0c;用一根网线连接主路由的LAN口和二级路由的WAN口&#xff08;当主路由使用配置类似&#xff09; 如果你想要作为旁路由或中继路由使用…

VUE中render渲染函数(h函数)

vue在绝大多数情况下都推荐使用模板来编写html结构,但是对于一些复杂场景下需要完全的JS编程能力&#xff0c;这个时候我们就可以使用渲染函数 &#xff0c;它比模板更接近编译器 vue在生成真实的DOM之前&#xff0c;会将我们的节点转换成VNode&#xff0c;而VNode组合在一起形…

巡更标签 “ PE29_BLE_XG

在我们日常中有一些场景涉及到打卡&#xff0c;比如一个设备需要维护&#xff0c;需要每天有工作人员到现场进行检查或者维护操作&#xff0c;目前普通的做法是弄个二维码到场扫码或者本子记录&#xff0c;用记录的方式明显太落后&#xff0c;容易导致监管不好操作&#xff0c;…

STM32 51单片机——搭建keil5的开发环境(ARM)

知识点&#xff1a;keil/proteus搭建概述、环境搭建 实训day1——12月19日 目录 1 keil安装 1.1 安装KEIL5 安装包 步骤1&#xff1a; 步骤2&#xff1a; 步骤3&#xff1a; 步骤4&#xff1a; 步骤5&#xff1a; 1.2 添加License 步骤1&#xff1a; 步骤2&#xff…

LabVIEW中忽略特定错误

LabVIEW中忽略特定错误 在LabVIEW中收到错误&#xff0c;但已经确认它不会对我的应用程序产生负面影响。如何忽略或清除此错误&#xff1f; LabVIEW程序因为出现错误而中止&#xff0c;但希望代码在收到此错误后继续。怎样才能做到这一点&#xff1f; 解决方案 忽略错误有三…

实验9 利用Wireshark软件分析DHCP

目录 一、实验目的及任务 二、实验环境 三、预备知识 四、实验步骤 五、实验报告内容 一、实验目的及任务 1.通过协议分析进一步明确DHCP报文格式中各字段语法语义&#xff1b; 2.进一步明确DHCP工作原理并能够描述 二、实验环境 联网的计算机&#xff1b;主机操作系…

iead中安装Lombok插件、Lombok注解的使用

Lombok插件安装&#xff1a; 1.pom.xml引入Lombok依赖包&#xff1a; <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.10</version></dependency>2.file-setting: 安装Lom…

Python中变量的定义和使用规则

一、如何理解Python中的变量 在解释变量这个东东之前&#xff0c;我先给大家看一组代码&#xff0c;如下图&#xff1a; 上图里面&#xff0c;a作为变量&#xff0c;每次存放的数据和数据类型都不同。看到这里大部分人应该明白了&#xff0c;变量就是随时都可以改变的量&#…

[附源码]计算机毕业设计Python的桌游信息管理系统(程序+源码+LW文档)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…