前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >老生常谈React的diff算法原理-面试版_2023-03-01

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

原创
作者头像
用户10358021
发布于 2023-03-01 00:11:55
发布于 2023-03-01 00:11:55
1K00
代码可运行
举报
文章被收录于专栏:前端面试2前端面试2
运行总次数:0
代码可运行

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- 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节点信息。
代码语言:txt
AI代码解释
复制
diff算法的本质上是对比1和4,生成2。
Diff的瓶颈以及React如何应对
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
由于diff操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中
将前后两棵树完全比对的算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量。

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

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

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

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

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

代码语言:typescript
AI代码解释
复制
 1.当newChild类型为object、numberstring,代表同级只有一个节点
- 2.当newChild类型为Array,同级有多个节点

单节点diff

代码语言:css
AI代码解释
复制
以类型Object为例,会进入这个函数reconcileSingleElement
代码语言:txt
AI代码解释
复制
这个函数会做如下事情:
代码语言:txt
AI代码解释
复制
让我们看看第二步判断DOM节点是否可以复用是如何实现的。

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

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

代码语言:yaml
AI代码解释
复制
1.当child !== null且key相同且type不同时执行deleteRemainingChildren将child及其兄弟fiber都标记删除。

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

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

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

解释:

代码语言:css
AI代码解释
复制
在reconcileSingleElement中遍历之前的3个fiber(对应的DOM为3个li),寻找本次更新的p是否可以复用之前的3个fiber中某个的DOM。

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

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

练习题:

代码语言:typescript
AI代码解释
复制
习题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:节点位置变化
代码语言:txt
AI代码解释
复制
注意在这里diff算法无法使用双指针优化
在我们做数组相关的算法题时,经常使用双指针从数组头和尾同时遍历以提高效率,但是这里却不行。
代码语言:css
AI代码解释
复制
虽然本次更新的JSX对象 newChildren为数组形式,但是和newChildren中每个组件进行比较的是current fiber
同级的Fiber节点是由sibling指针链接形成的单链表。

即 newChildren[0]与fiber比较,newChildren[1]与fiber.sibling比较。
代码语言:txt
AI代码解释
复制
所以无法使用双指针优化。

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

代码语言:txt
AI代码解释
复制
1.第一轮遍历:处理更新的节点。
2.第二轮遍历:处理剩下的不属于更新的节点

第一轮遍历:

第一轮遍历步骤如下:

代码语言:css
AI代码解释
复制
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也没有遍历完。

上例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
2个节点可复用,遍历到key === 2的节点发现type改变,不可复用,跳出遍历。

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

上面4跳出的遍历

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

上例子:

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

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

代码语言:shell
AI代码解释
复制
- 1.newChildren与oldFiber同时遍历完 

那就是最理想的情况:只有组件更新。此时Diff结束。
代码语言:shell
AI代码解释
复制
- 2.newChildren没遍历完,oldFiber遍历完

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

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

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

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

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

为了快速的找到key对应的oldFiber,我们将所有还未处理的oldFiber存入以key为key,oldFiber为value的Map中。
代码语言:text
AI代码解释
复制
接下来遍历剩余的newChildren,通过newChildren[i].key就能在existingChildren中找到key相同的oldFiber

标记节点是否移动

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

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

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

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

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

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

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

demo1

// 之前 abcd // 之后 acdb

===第一轮遍历开始===

代码语言:css
AI代码解释
复制
a(之后)vs a(之前)  

key不变,可复用

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

所以 lastPlacedIndex = 0;

继续第一轮遍历...

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
c(之后)vs b(之前)  

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

此时 lastPlacedIndex === 0;

===第一轮遍历结束===

===第二轮遍历开始===

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
newChildren === cdb,没用完,不需要执行删除旧节点

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

将剩余oldFiber(bcd)保存为map

// 当前oldFiber:bcd

// 当前newChildren:cdb

继续遍历剩余newChildren

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
key === d 在 oldFiber中存在

const oldIndex = d(之前).index;

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

则 lastPlacedIndex = 3;

d节点位置不变

继续遍历剩余newChildren

// 当前oldFiber:b

// 当前newChildren:b

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
key === b 在 oldFiber中存在

const oldIndex = b(之前).index;

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

则 b节点需要向右移动

===第二轮遍历结束===

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

demo2

// 之前 abcd

// 之后 dabc

===第一轮遍历开始===

代码语言:css
AI代码解释
复制
d(之后)vs a(之前)  
key改变,不能复用,跳出遍历

===第一轮遍历结束===

===第二轮遍历开始===

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
newChildren === dabc,没用完,不需要执行删除旧节点

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

将剩余oldFiber(abcd)保存为map

继续遍历剩余newChildren

// 当前oldFiber:abcd

// 当前newChildren dabc

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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的后面。

用张老生常谈的图

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
暂无评论
推荐阅读
维基链WICC | WaykiBridge,实现多端无缝体验维基链DApp
随着智能手机的普及,移动互联网已深入生活的方方面面。用户也更习惯直接通过手机来使用各种应用程序,包括DApp。手机钱包作为区块链世界的“支付宝”,担负着保存私钥的重要作用,用它来连接DApp服务端和终端用户,成为了很自然的选择。
维基链WICC
2019/05/17
6510
维基链WICC | WaykiBridge,实现多端无缝体验维基链DApp
Ethereum 核心技术解读
比特币作为一种去中心化的数字货币,是极其成功的,但受限于比特币脚本(非图灵完备,只能处理一些简单的逻辑),并不能处理很复杂的业务。而Ethereum引入了智能合约,使去中心化的概念能够应用于更丰富的应用场景,因此也被称为区块链 2.0。本文将对以太坊核心技术进行解读,如有错漏,欢迎交流指正。
pseudoyu
2023/04/11
7200
Ethereum 核心技术解读
游戏领域区块链探索
中国广东省深圳市龙华新区民治街道溪山美地 518131 +86 13113668890 <netkiller@msn.com>
netkiller old
2018/03/07
2.8K0
以太坊DApp系列(二)---从入门到出家
以太坊自2013年V神提出后,被无数人赋予美好的愿景,甚至被称为区块链2.0,其代币发行量更是达到了全球第二,仅次于比特币,而其带来的智能合约概念颠覆了人们对区块链的理解,让区块链不仅仅是个账本,更像一个操作系统,赋予了每个节点“智能”。经过差不多半年来断断续续的学习、理解和沉淀,笔者今天想揭开以太坊DApp神秘的面纱,看看以太坊是猴还是猿。
forrestlin
2018/07/17
3.6K0
以太坊DApp系列(二)---从入门到出家
用Python实现一个基于RSA算法的区块链客户端(区块链系列4)
编译 | 晚君、Molly、蒋宝尚 来源 | BlockChange 区块链作为比特币和其他加密货币的核心技术,在最近几年引起了全世界的注意,但是各国这一颠覆性的技术态度不一,因为其去中心化的分布式结构,可以使用户之间直接进行交流,无需中心节点参与的这种技术模式对银行、证券等机构带来了极大影响。 在本篇文章,抛开介绍区块链的技术特点和应用场景,手把手的教大家如何用python实现一个基础的区块链,和一个区块链的客户端。 我们实现的区块链有如下几个特性: 可以向区块链中添加多个节点。 工作量证明(PoW)
量化投资与机器学习微信公众号
2018/05/28
1.5K0
使用Substrate开发区块链存证应用V2.0
本文是《使用Substrate开发区块链存证dApp》一文的更新,在一台全新服务器上,从零起步,采用最新的v2.0.0版本开发一个自定义的区块链存证dApp。
jasonruan
2020/12/14
1.4K0
10分钟,前端工程师也能玩转区块链Web3.js开发
一个不想写后台的前端不是一个好全栈,前端也可以玩转区块链Web3.js开发。 老吴(北京志顶科技有限公司技术总监、区块链专家)针对前端工程师如何玩转Web3.js开发后端钱包这一主题,分享了自己开
区块链大本营
2018/06/19
3.7K0
阿桑奇被捕,“维基解密”官方BTC地址捐赠数剧增;芒果互娱《快乐大本营》引入区块链技术的IP数字化服务 | 一分钟链圈
据环球网消息,近日,芒果互娱与创无限就IP数字化达成合作,率先在《快乐大本营》落地。由国内专业数字资产登记服务机构中证数登,为《快乐大本营》提供IP数字化登记、授权管理服务,采用区块链技术从源头解决IP盗版、侵权、衍生品销售无法追踪等问题。
区块链大本营
2019/04/28
8940
《以太坊攻略》,小白如何逆袭成为技术大咖?要学的全在这里了
ConsenSys产品经理认为,区块链新手和经验丰富的区块链开发人员,需要共享工具、开发模式和组件。
区块链大本营
2018/09/21
1.9K0
《以太坊攻略》,小白如何逆袭成为技术大咖?要学的全在这里了
以太坊 Truffle 测试代币锁仓,转账,空投
中国广东省深圳市龙华新区民治街道溪山美地 518131 +86 13113668890 <netkiller@msn.com>
netkiller old
2018/05/17
3.2K0
以太坊 Truffle 测试代币锁仓,转账,空投
使用Substrate开发区块链存证dApp
前面文章介绍了在Substrate上开发智能合约,包括使用原生的ink!语言开发ERC20智能合约,以及将以太坊的Solidity智能合约跑在Substrate链上,在本文将进一步学习在Substrate链上开发一个自定义的区块链存证dApp。
Tiny熊
2020/08/10
1.4K0
【葵花宝典】区块链技术面试必考题 01 区块链面试真经
话说,区块链行业对人才的缺口越来越大,但由于区块链涉及的知识领域较为广泛,能找到真正有用的人才对每个企业来说都非常不易。
辉哥
2018/10/18
2K0
【葵花宝典】区块链技术面试必考题
01 区块链面试真经
区块链技术详解和Python实现案例
区块链可以说是互联网成立以来最重要和最具颠覆性的技术之一。它是比特币和其他加密货币背后的核心技术,在过去几年引起大家广泛的关注。 区块链的核心是一个分布式数据库,允许双方直接交易,而无需中央机构,也就是通常大家所说的"去中心化"。"去中心化"这个简单而重要的概念对银行、政府和市场等机构具有重大意义,可以说,任何依赖中央数据库作为核心竞争优势的企业或组织都可能受到区块链技术的挑战甚至颠覆。 本文的目标是给你一个区块链技术的实用介绍,而不是炒作比特币和其他加密货币概念。第1节和第2节介绍了区块链一些核心概念
小莹莹
2018/04/18
2.5K0
区块链技术详解和Python实现案例
.netcore如何开发以太坊区块链示例 原
本文描述了在dotNet核心中使用像以太坊这样的区块链平台的过程。目标受众是其他想要从以太坊开始的dotNet开发者。需要了解区块链。在本文中,我们构建了一个完整的示例,允许你与自定义编写的智能合约进行交互。
笔阁
2018/10/26
1.4K0
第5课 EOS环境搭建入门(私链节点-钱包-密钥-账号)
【本文目标】 通过本文实践,能在已编译的EOS V1.0.5版本环境上,完成私链节点启动,钱包创建,密钥导入和账号创建等内容。 【前置条件】 你已完成了EOS编译,编译测试成功。未完成的可参考《第4课 如何在UBUNTU虚拟机上编译EOS完成环境搭建?》完成相关配置。 【技术收获】 1)EOS的节点,钱包,密钥,账号的概念和理解 2)EOS钱包/账号的建立和遇到的问题分析及解决方法 【说明】 EOS版本还没有稳定下来,即使完成了V1.0.2版本环境搭建的人,到V1.0.5时还是摔在了坑里。辉哥通过踩坑分析给大家提供尽可能多的知识和解决思路,大家在V1.0.5以后的版本部署可参考文章和以错误关键字搜索官网的issue网址获取更多知识。
辉哥
2018/08/10
1.7K0
第5课 EOS环境搭建入门(私链节点-钱包-密钥-账号)
区块链3.0:拥抱EOS
EOS是当下最火的区块链技术,被社会广泛看好为下一代区块链3.0。不同于以太坊的学习,EOS的主语言是C++,本文作为EOS研究的首篇文章,重点介绍EOS的创新点,它的周边生态,各种概念原理的解释,以及它被看好的原因。而针对EOS的源码学习,原理实现以及并行的C++语言的快速学习与掌握,我会在接下来制定一系列学习计划一一付诸实现。 关键字:EOS,DAPP,石墨烯技术,构建本地节点,公链映射,选举,EOS链配置,术语解释 EOS.IO EOS.IO 是由block.one开发的一个基于区块链结
文彬
2018/05/03
3.1K0
面向企业的区块链教程(一)
区块链正在迅速增长,并改变着商业的运作方式。领先的组织已经在探索区块链的可能性。通过本书,你将学会如何构建端到端的企业级去中心化应用程序(DApps)并在组织中扩展它们以满足公司的需求。
ApacheCN_飞龙
2024/05/24
3370
面向企业的区块链教程(一)
以太坊开发工具及资源大全
以太坊开发工具大全 - 包含 250 多个推荐的开发工具、代码库、工具站点。涵盖内容包含:合约开发、测试、安全分析、数据分析、开发框架、测试网络、开发范式等。
Tiny熊
2021/01/28
2.6K0
第十一课 从宠物商店案例看DAPP架构和WEB3.JS交互接口
【本文目标】 了解ETH生态下DAPP去中心化应用程序的框架和交互流程,了解WEB3.JS的作用和接口函数。 【前置条件】 完成了《第六课 技术小白如何开发一个DAPP区块链应用(以宠物商店为例)》的学习实践,对智能合约了解。 【技术收获】 1). DAPP架构 2). ETH节点框架 3).宠物商店的APP.js文件的业务处理流程图和函数介绍 4).web3.js接口
辉哥
2018/08/10
2.7K0
第十一课 从宠物商店案例看DAPP架构和WEB3.JS交互接口
必读!未来月薪10万的五大利器(一)
当前,区块链技术已经由1.0版本过渡到2.0版本,并逐步向3.0版本发展。新一代区块链技术发展的主要方向侧重于基础设施建设,即区块链底层技术的研发以及一些具体应用的落地。区块链3.0技术发展的目的在于提高区块链的整体运行性能,包括通过各种方式提高区块链系统的交易容量、交易速度以及系统的可扩展性等。
区块链大本营
2019/04/28
5420
必读!未来月薪10万的五大利器(一)
推荐阅读
相关推荐
维基链WICC | WaykiBridge,实现多端无缝体验维基链DApp
更多 >
目录
  • Diff的瓶颈以及React如何应对
  • 所以为了降低算法复杂度,React的diff会预设3个限制:
  • 那么我们接下来看一下Diff是如何实现的
  • 我们可以从同级的节点数量将Diff分为两类:
  • 下面通过两个demo来看一下react团队的diff算法精髓
    • demo1
    • demo2
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档