react源码-diff
diff
在render
阶段更新Fiber
节点时,我们会调用reconcileChildFibers
对比current Fiber
和jsx
对象构建workInProgress Fiber
,这里current Fiber
是指当前dom
对应的fiber
树,jsx
是class
组件render
方法或者函数组件的返回值。
1 | function reconcileChildFibers( |
我们知道对比两颗树的复杂度本身是O(n3)
,对我们的应用来说这个是不能承受的量级,react为了降低复杂度,提出了三个前提:
只对同级比较,跨层级的dom不会进行复用
不同类型节点生成的dom树不同,此时会直接销毁老节点及子孙节点,并新建节点
可以通过key来对元素diff的过程提供复用的线索,例如:
1
2
3
4
5
6
7
8
9
10
11
12const a = (
<>
<p key="0">0</p>
<p key="1">1</p>
</>
);
const b = (
<>
<p key="1">1</p>
<p key="0">0</p>
</>
);
如果a
和b
里的元素都没有key
,因为节点的更新前后文本节点不同,导致他们都不能复用,所以会销毁之前的节点,并新建节点,但是现在有key
了,b
中的节点会在老的a
中寻找key
相同的节点尝试复用,最后发现只是交换位置就可以完成更新,具体对比过程后面会讲到。
单节点diff
单点diff有如下几种情况:
- key和type相同表示可以复用节点
- key不同直接标记删除节点,然后新建节点
- key相同type不同,标记删除该节点和兄弟节点,然后新创建节点
1 | function reconcileSingleElement( |
多节点diff
多节点diff比较复杂,我们分三种情况进行讨论,其中a表示更新前的节点,b表示更新后的节点
属性变化
1
2
3
4
5
6
7
8
9
10
11
12const a = (
<>
<p key="0" name='0'>0</p>
<p key="1">1</p>
</>
);
const b = (
<>
<p key="0" name='00'>0</p>
<p key="1">1</p>
</>
);type变化
1
2
3
4
5
6
7
8
9
10
11
12const a = (
<>
<p key="0">0</p>
<p key="1">1</p>
</>
);
const b = (
<>
<div key="0">0</div>
<p key="1">1</p>
</>
);新增节点
1 | const a = ( |
- 节点删除
1 | const a = ( |
节点位置变化
1
2
3
4
5
6
7
8
9
10
11
12const a = (
<>
<p key="0">0</p>
<p key="1">1</p>
</>
);
const b = (
<>
<p key="1">1</p>
<p key="0">0</p>
</>
);
在源码中多节点diff
会经历三次遍历,第一次遍历处理节点的更新(包括props
更新和type
更新和删除),第二次遍历处理其他的情况(节点新增),其原因在于在大多数的应用中,节点更新的频率更加频繁,第三次处理位节点置改变
第一次遍历
因为老的节点存在于current Fiber
中,所以它是个链表结构,还记得Fiber
双缓存结构嘛,节点通过child
、return
、sibling
连接,而newChildren
存在于jsx
当中,所以遍历对比的时候,首先让newChildren[i]
与oldFiber
对比,然后让i++
、nextOldFiber = oldFiber.sibling
。在第一轮遍历中,会处理三种情况,其中第1,2两种情况会结束第一次循环
key
不同,第一次循环结束newChildren
或者oldFiber
遍历完,第一次循环结束key
同type
不同,标记oldFiber
为DELETION
key
相同type
相同则可以复用
newChildren
遍历完,oldFiber
没遍历完,在第一次遍历完成之后将oldFiber
中没遍历完的节点标记为DELETION
,即删除的DELETION Tag
第二次遍历
第二次遍历考虑三种情况
newChildren
和oldFiber
都遍历完:多节点diff
过程结束newChildren
没遍历完,oldFiber
遍历完,将剩下的newChildren
的节点标记为Placement
,即插入的Tag
newChildren
和oldFiber
没遍历完,则进入节点移动的逻辑
第三次遍历
主要逻辑在
placeChild
函数中,例如更新前节点顺序是ABCD
,更新后是ACDB
newChild
中第一个位置的A
和oldFiber
第一个位置的A
,key
相同可复用,lastPlacedIndex=0
newChild
中第二个位置的C
和oldFiber
第二个位置的B
,key
不同 跳出第一次循环,将oldFiber
中的BCD
保存在map
中newChild
中第二个位置的C
在oldFiber
中的index=2 > lastPlacedIndex=0
不需要移动,lastPlacedIndex=2
newChild
中第三个位置的D
在oldFiber
中的index=3 > lastPlacedIndex=2
不需要移动,lastPlacedIndex=3
newChild
中第四个位置的B
在oldFiber
中的index=1 < lastPlacedIndex=3
,移动到最后
例如更新前节点顺序是
ABCD
,更新后是DABC
newChild
中第一个位置的D
和oldFiber
第一个位置的A
,key
不相同不可复用,将oldFiber
中的ABCD
保存在map
中,lastPlacedIndex=0
newChild
中第一个位置的D
在oldFiber
中的index=3 > lastPlacedIndex=0
不需要移动,lastPlacedIndex=3
newChild
中第二个位置的A
在oldFiber
中的index=0 < lastPlacedIndex=3
,移动到最后newChild
中第三个位置的B
在oldFiber
中的index=1 < lastPlacedIndex=3
,移动到最后newChild
中第四个位置的C
在oldFiber
中的index=2 < lastPlacedIndex=3
,移动到最后
代码
1 | function placeChild(newFiber, lastPlacedIndex, newIndex) { |
1 | function reconcileChildrenArray( |