reconcileChildrenArray是react对于多节点diff进行操作的函数
在了解diff之前先需要了解一下fiber
fiber
在react中通过jsx生成虚拟dom对象,然后通过虚拟dom对象生产fiber对象,最后通过fiber生产视图。
大致流程
jsx
->vdom
->fiber
源码在react/src/ReactFiber.old.js
js
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
...todo
}
fiber是链表结构,反应的真实dom的位置关系。 以下几个属性是和本文比较有关系。
key
节点的唯一标识符,用于进行节点的比较更新。就是在jsx标签中写的key
elementType
大部分情况与type
相同,某些情况不同,比如 FunctionComponent 使用 React.memo 包裹,表示元素的类型type
表示元素的类型, 对于 FunctionComponent,指函数本身,对于ClassComponent,指 class,对于 HostComponent,指 DOM 节点 tagNamestateNode
FiberNode 对应的真实 DOM 节点return
指向该 FiberNode 的父节点child
指向该 FiberNode 的第一个子节点sibling
指向右边第一个兄弟 Fiber 节点index
当前节点在父节点的子节点列表中的索引
jsx
function App() {
const [list, setlist] = useState([
{ name: "zhangsan", id: 'a' },
{ name: "lisi", id: 'b' },
{ name: "wawngwu", id: 'c' },
]);
return (
<p>
{list.map((item, index) => (
<span key={item.id}>{item.name}</span>
))}
</p>
);
}
export default App;
代码对应草图

了解完fiber接下来要讲的就是多节点diff了,这里的代码在核心在react/src/ReactChildFiber.old.js
的reconcileChildrenArray
函数中
jsx
if (isArray(newChild)) {
return reconcileChildrenArray(
//workInProgress tree
returnFiber,
//生成workInProgress tree的current tree的child,参考上图的关系
currentFirstChild,
//新生成的虚拟dom
newChild,
lanes,
);
}
returnFiber
是需要更新的oldfiber
的workInProgress
(这里需要说一下在更新的时候每个fiber
对象都会创建workInProgress
对象,就相当于fiber的镜像。后续所有的更新都在workInProgress
上,更新完成之后workInProgress
就代表最后dom的状态)
jsx
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
//作为子链表的第一个fiber的返回
//最后return resultingFirstChild
let resultingFirstChild: Fiber | null = null;
//储存上一次操作的fiber
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
//被更新的节点
let lastPlacedIndex = 0;
//遍历的索引
let newIdx = 0;
//下一个要操作的fiber
let nextOldFiber = null;
}
通过以下demo来演示多点diff过程
jsx
function App() {
useEffect(()=>{
setTimeout(()=>{
setlist([
{ name: "zhangsan", id: 'a' },
{ name: "wawngwu", id: 'c' },
{ name: "lisi", id: 'b'},
])
},1000)
},[])
const [list, setlist] = useState([
{ name: "zhangsan", id: 'a' },
{ name: "lisi", id: 'b'},
{ name: "wawngwu", id: 'c' },
]);
return (
<p>
{list.map((item, index) => (
<span key={item.id}>{item.name}</span>
))}
</p>
);
}
export default App;
节点示意图如下

jsx
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
//因为是数组,所以需要保存需要被操作的下一个节点内容
nextOldFiber = oldFiber.sibling;
}
//两种情况
//oldFiber.key === newChildren[newIdx] 返回更新后的节点
//oldFiber.key !== newChildren[newIdx] 返回null
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
//代表本次key没有匹配到,直接跳出循环
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
//删除节点
deleteChild(returnFiber, oldFiber);
}
}
//移动操作
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
//第一次循环
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
//当前循环操作的fiber节点
previousNewFiber = newFiber;
//把之前储存的oldfiber.sibing作为本次循环的oldfiber
oldFiber = nextOldFiber;
}
lastPlacedIndex是一种顺序优化手段,lastPlacedIndex 一直在更新,它表示旧集合中在当前最大的值,也就是最靠近右边的位置
,如果新集合的访问的fiber
节点的index < lastPlacedIndex
说明当前访问的节点比上一个节点靠后
,不需要移动,否则的话需要移动。
第一轮遍历
oldFiber.index = 0
newIdx=0
nextOldFiber = fiber span(key=b)
进入updateSlot
对fiber span(key=a)
和 newChildren[0]
的比较
jsx
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
const key = oldFiber !== null ? oldFiber.key : null;
if (newChild.key === key) {
//key相同的话返回更新过后的fiber节点
return updateElement(returnFiber, oldFiber, newChild, lanes);
} else {
//不相同的话返回null
return null;
}
...
}
第二轮遍历
oldFiber = fiber span(key=b)
oldFiber.index = 1
newIdx=1
nextOldFiber = fiber span(key= c)
进入updateSlot
对fiber span(key=b)
和 newChildren[1]
的比较
发现key
不同,说明不是同一个节点,退出循环
jsx
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
此时
oldFiber = fiber span(key=b)
oldFiber.index = 1
nextOldFiber = fiber span(key= c)
newChildren.length=3
进入下一个环节
jsx
//不匹配
if (newIdx === newChildren.length) {
}
//不匹配
// if (oldFiber === null) {
}
//通过遍历oldfiber以及他的silbing得到一个键值为{key:fiber}的map对象
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

得到oldfiber
后继续遍历新集合
第二轮遍历
oldFiber = fiber span(key=b)
newChildren.length=3
jsx
for (; newIdx < newChildren.length; newIdx++) {
//第一次遍历,发现map里有newChildren[newIdx].key的成员,返回新的fiberNode
//第二次遍历,发现map里有newChildren[newIdx].key的成员,返回新的fiberNode
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
//?
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
//获得新的fiber,把map里的对应map删除
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
//newidx作为newFiber.index,代表fiber在链表中的顺序,
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
//把兄弟节点连接起来
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
经过两轮遍历,对于当前变化的节点就都已经处理完毕。
例子
jsx
变化前 abcd
变化后 bcead
第一轮遍历, a !== b ,退出循环
创建`map`,
{
a:fiberA,
b:fiberB,
c:fiberC,
d:fiberD
}
此时
newIdx = 0
lastPlaceIndex = 0
newChildren = bcead
遍历`newChildren`
### 第一遍
map中发现b节点,此时`oldindex = 1` , 更新`b节点workInprogress`,因为 `oldindex > lastPlaceIndex `所以要更新`lastPlaceIndex`的值,然后删除`map`里`b节点`
newIdx = 0
lastPlaceIndex = 1
newChildren = bcead
### 第二遍
map中发现b节点,此时`oldindex = 2`,更新`c节点workInprogress`,因为 `oldindex > lastPlaceIndex `所以要更新`lastPlaceIndex`的值,然后删除`map`里`c节点`
newIdx = 1
lastPlaceIndex = 2
newChildren = cead
### 第三遍
`map`没有发现`c节点`,需要创建`c节点workInprogress`,因为是新增节点,所以`lastPlaceIndex`不变,将`workInprogress`标记为Placement
newIdx = 2
lastPlaceIndex = 2
newChildren = ead
### 第四遍
`map`发现`a节点`,此时`oldindex = 1`,需要更新`a节点workInprogress`,因为 `oldindex < lastPlaceIndex `,所以需要移动节点,将`workInprogress`标记为Placement
newIdx = 3
lastPlaceIndex = 2
newChildren = ad
### 第四遍
`map`发现`d节点`,此时`oldindex = 3`,需要更新`d节点workInprogress`,因为 `oldindex > lastPlaceIndex `,所以要更新`lastPlaceIndex`
newIdx = 4
lastPlaceIndex = 3
newChildren = d
通过diff,更新了三次节点,移动了一个节点,插入了一个节点
总结
多节点diff有以下两种情况
1.第一轮可以遍历完新集合或者老节点
1.newIdx === newChildren.length,只需要需要删除老节点

2.oldFiber === null 最后操作的一个fiber的silbing为null,继续遍历新集合,创建对应的新节点。

2.第一轮无法遍历完新集合和老节点
- 第一轮遍历,对集合进行遍历,使用key去比较当前操作的fiberNode的key,如果匹配的话则更新节点,不匹配的话跳出遍历,进行下一步操作
2. 第二轮遍历,对当前的fiber及其他的兄弟节点进行以{key:fiberNode}的形式,存储在map对象中,继续遍历剩余的集合。查询map中是否有相同的key,如果有的话进行节点复用,再根据lastPlaceIndex来决定节点是否需要移动,没有的话就创建新的fiberNode,每次遍历删除掉map里已经被更新的key。

- 经过第二轮遍历说明新节点已经更新/创建完成。map里剩余的就是需要被删除的节点,只需要遍历map,把workInProgress里相同的节点删除,这一轮遍历是根据新集合的顺序