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

第一次发文章 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中情况的一种或多种。

参考:前端react面试题详细解答

  • 情况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<
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React diff算法React用来在虚拟DOM树中找到变化并更新实际DOM的一种算法。它的原理是通过比较新旧虚拟DOM树的差异,然后只更新真正发生变化的部分,而不是直接重新渲染整个页面。 React diff算法的高阶原理是通过三个步骤来进行差异比较: 1. 树的遍历:首先,React会对新旧两棵虚拟DOM树进行深度优先遍历,找出所有的节点并进行标记。 2. 节点的比较:在遍历过程中,React会比较新旧两个节点的类型(标签名)和属性。如果类型相同且属性相同,则认为这个节点是相同的,不需要更新。如果类型不同,则直接替换该节点。如果类型相同但属性不同,则更新该节点的属性。 3. 子节点的递归比较:如果两个节点相同,并且有子节点,则会对子节点进行递归比较。React会对子节点列表进行遍历,并在新旧子节点列表中找出相同的节点进行比较。如果找到了相同的节点,则继续递归比较其子节点。如果没有找到相同的节点,则说明这是一个新增或者删除的节点,需要进行相应的操作。 通过这种方式,React diff算法可以高效地找到变化的节点并进行更新,避免了无谓的重复渲染,提升了性能。但是需要注意的是,React diff算法并不是完全精确的,有时候可能会出现误判或者不够高效的情况,所以在开发中还需要注意一些优化策略,例如使用key属性来帮助React更准确地识别节点的变化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值