likes
comments
collection
share

React源码系列(六)------ dom-diff

作者站长头像
站长
· 阅读数 78

前言

今日的主题是React的绝对核心DOM-DIFF,想要了解React的更新流程,就非常有必要理解DOM-DIFF。废话不多说,我们直接开始吧。

DOM-DIFF

在聊具体如何diff之前,我们先来了解diff的几个原则。

  • 只对同级元素diff
  • 类型(fiber.type)不同就视为不同的fiber,不复用
  • key(身份证)相同表示是同一个fiber

在React源码中,可以很明显看出来它区分单节点和多节点的处理。

React源码系列(六)------ dom-diff

其中主要原因还是因为DOM-DIFF,单节点的DOM-DIFF是非常简单的。

单节点DOM-DIFF

DOM-DIFF主要就是为了尽可能复用fiber,减少dom操作,再结合上述原则,我们可以很容易猜出单节点diff的具体过程。

  1. 先比较key
  2. 再比较type

我们来直接看看单节点的整个流程。

React源码系列(六)------ dom-diff

看完上面这张流程图,或许会疑惑这几处地方。

React源码系列(六)------ dom-diff

不是单节点么,无论是发现可复用或者不可复用,为何还能有下一个节点或者删除剩下的节点?单节点哪来的其他fiber?

其实所谓单节点是对于新的父fiber来讲的,可以看到开始节点是判断有没老fiber(替身),也就是说当前父fiber的替身很可能有多个child,也就是说这个fiber的更新很可能是从多节点变单节点,所以在单节点DOM-DIFF中,目标就是要在老的child/children里面找到那个可复用的,如果找到了当然就要删掉其他的child了,毕竟我们只需要一个。

单节点DOM-DIFF较为简单,在此就过多解释,流程图讲的是非常清楚了。多跟着流程图走两遍必能完全搞明白。

多节点DOM-DIFF

本文的主要难点来了,多节点的diff是较为复杂的,在正式将多节点diff的代码流程之前,我们先通过一个简单的例子模拟一下多节点diff。

例子

假设我们是一个小学堂的老师,我们要给我们仅有的4位学生换位。

React源码系列(六)------ dom-diff

看到换位后的这个座位表,我们应该怎么调整他们的座位呢?

  1. 先尽可能找到不需要动的。

React源码系列(六)------ dom-diff

  1. 尽可能减少移动,找出需要移动的

什么叫找出需要移动的呢?对比新旧座位表,我们发现四号无论是新旧座位表都保持着这样一个关系,四号前面都是比他索引要小的,所以四号就属于不需要移动的了,而在新座位表中,凡是旧索引比四号的旧索引要小的,通通都是需要移动的。

React源码系列(六)------ dom-diff

此时就产生了一个全局变量---lastPlacedIndex。这个变量就记录着上一个不需要移动的那个同学的旧索引。其他同学就依据这个lastPlacedIndex来判断自己是不是需要移动的。如果自己的旧索引比lastPlacedIndex要小,那么自己就是要移动的,反之就是不用移动的。

React源码系列(六)------ dom-diff

三号的旧索引index比lastPlacedIndex要小,所以他要动,直接执行一个appendChild,插入到当前队伍的最后面。

React源码系列(六)------ dom-diff

二号的旧索引index一样比lastPlacedIndex小,所以他也要动,同样执行一个appendChild,插入到当前队伍的最后面。到这里,整个换位就完成了。

复杂一点的真实DOM-DIFF流程例子

看完上面那个例子,相信大家已经对多节点DOM-DIFF有一个大概的了解了,我们来看一个完整一点的DOM-DIFF的例子。

前置知识:

  1. 下面这个例子假设的是所有fiber的type都相同,因为type不同也只是直接重新创建这个fiber,和key不同走的流程差不多,所以就不增加复杂度了。
  2. 整个DOM-DIFF过程是发生在beginWork阶段,所以DOM-DIFF只是给这些子节点们打上一些操作标识,还不会真正去执行这些操作。
  3. 因为fiber节点间都是一个链表形式,兄弟节点间通过sibling指向下一位,所以这里多了一个新的全局变量previousFiber,用来帮助新的children形成兄弟间的单向链表(具体看下面流程图)。
  4. 在DOM-DIFF期间标识的删除操作会在commitRoot阶段执行,标识的插入操作会在completeWork阶段执行。

例子:

我们来看看这个新的座位表,一起再走一遍例子一的流程,但换成代码术语。

React源码系列(六)------ dom-diff

第一步:遍历旧节点们,根据索引位置,一对一比较,直到第一个不能完全复用的。

React源码系列(六)------ dom-diff

1.1、旧的0索引上的是A的旧节点(A1),新的0索引上的是A的新节点(A2),它们无论是key,type还是index都一致,完全能复用,将previousFiber更新为A2,遍历继续。

React源码系列(六)------ dom-diff

1.2、旧的1索引上的是B的旧节点(B1),新的1索引上的是C的新节点(C2),他们各方面都不同,不能复用,遍历结束,将剩下的旧节点们都存起来,放到Map中,第一步结束。

第二步:遍历剩余的新children,与自己的老child做比较,找出需要移动的。

React源码系列(六)------ dom-diff

2.1、在Map中找到C2的旧节点C1,比较旧索引index(2)和lastPlacedIndex(0),index大,索引C1属于不需要移动的,复用C1节点,然后将lastPlacedIndex更新为2,且让previousFiber(A2).sibling指向为C2,然后将previousFiber更新为C2,然后再将Map中的C1删除,遍历继续。

React源码系列(六)------ dom-diff

2.2、同理2.1。

React源码系列(六)------ dom-diff

2.3、在Map中找到旧节点B1,index比lastPlacedIndex小,说明要移动,复用B1,然后打上插入标识,从Map中删除B1,且让previousFiber(C2).sibling指向B2,然后将previousFiber更新为B2。

React源码系列(六)------ dom-diff

2.4、从Map中找不到G1节点的老节点,所以直接新建一个G1的fiber,并打上插入操作标识,让preciousFiber(B2).sibling指向G1,然后将previousFiber更新为G1。

React源码系列(六)------ dom-diff

2.4、同理2.3。由于新children全部遍历完毕,第二步结束。

第三步:检查Map中是否还有节点,若有全部打上删除标识。

React源码系列(六)------ dom-diff

3.1、发现Map中还有一个F节点,给F节点打上删除标识。结束DOM-DIFF。

这个例子基本就是整个DOM-DIFF的完整流程了,他可能没包括所有的情况,但基本都是差不多,总的来说就是三轮遍历,第一轮索引遍历,第二轮剩余的新节点遍历,第三轮是剩余的老节点遍历。

代码层面的完整流程

若是通过上述的两个例子还觉得有疑点,或者那些地方没理解的,可以查看下面这个流程图。

React源码系列(六)------ dom-diff

结尾

本文分别从单节点和多节点两方面来讲DOM-DIFF,通过多个举例说明和多张流程图去描述DIFF过程,相信各位读者一定能从中有所收获。

以下就是本系列的惯例,包含至DOM-DIFF版的代码和完整版源码供各位调试。

仅包含至DOM-DIFF的代码:点这里

完整版源码:点这里

上一篇:useReducer

转载自:https://juejin.cn/post/7242876385734803513
评论
请登录