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标签中写的keyelementType大部分情况与type相同,某些情况不同,比如 FunctionComponent 使用 React.memo 包裹,表示元素的类型type表示元素的类型, 对于 FunctionComponent,指函数本身,对于ClassComponent,指 class,对于 HostComponent,指 DOM 节点 tagNamestateNodeFiberNode 对应的真实 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里相同的节点删除,这一轮遍历是根据新集合的顺序