两种节点类型
我们可以从同级的节点数量将Diff分为两类:
当newChild类型为object、number、string,代表同级只有一个节点
当newChild类型为Array,同级有多个节点
在接下来两节我们会分别讨论这两类节点的Diff,注意这里的单节点是指虚拟dom节点是个单或者多节点,可以简单看做是不是返回的数组
单节点
单节点比较还是比较简单的
//删除节点
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
if (!shouldTrackSideEffects) {
// Noop.
return;
}
//effect链的处理
const last = returnFiber.lastEffect;
if (last !== null) {
last.nextEffect = childToDelete;
returnFiber.lastEffect = childToDelete;
} else {
//证明暂时还没有形成链需要第一个节点
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
}
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete];
// TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions)
returnFiber.effectTag |= Deletion;
} else {
deletions.push(childToDelete);
}
childToDelete.nextEffect = null;
}
//批量删除节点的工具函数(更准确的是批量标记)
function deleteRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
): null {
if (!shouldTrackSideEffects) {
// Noop.
return null;
}
// TODO: For the shouldClone case, this could be micro-optimized a bit by
// assuming that after the first child we've already added everything.
let childToDelete = currentFirstChild;
while (childToDelete !== null) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
return null;
}
//element其实就是新的虚拟dom
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 首先判断是否存在对应DOM节点
while (child !== null) {
// 上一次更新存在DOM节点,接下来判断是否可复用
// 首先比较key是否相同
if (child.key === key) {
// key相同,接下来比较type是否相同
switch (child.tag) {
// ...省略case
default: {
if (child.elementType === element.type) {
// type相同则表示可以复用
deleteRemainingChildren(returnFiber, child.sibling);//显然这个节点的后续节点都必须删除了 因为找到了
const existing = useFiber(child, element.props);//useFiber故名思义 这里的element.props就是后续看是否要调整的属性
// 返回复用的fiber
return existing;
}
// type不同则跳出switch
break;
}
}
// 代码执行到这里代表:key相同但是type不同
// 将该fiber及其兄弟fiber标记为删除
deleteRemainingChildren(returnFiber, child);
break;
} else {
// key不同,将该fiber标记为删除
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 创建新Fiber,并返回 ...省略
}
可以发现需要被删除的fiber 不会在这直接真的删除,而是形成一个effect链,另外父节点会维护一个deletions的fiber数组
首先判断child是否存在,不存在则直接开始兄弟节点的比较,while终止在同层比较完成后
几种逻辑分支
- key相同,类型也相同直接可复用,后续就看属性情况更新属性即可
- key相同,类型不同了,直接deleteRemainingChildren 删除这个节点及他的兄弟节点,这里是因为key相同了,后续没有继续比较找可复用节点的意义了,故把原节点删完就可以了
- key不同,直接把这个比较的节点删除
多节点
先整理一下源码
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
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) {
//构建新的fiber链作为返回值
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {//构建新的fiber链作为返回值
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
先对几个用到的重要函数解读一下
updateSlot这个函数可以简单理解为节点比较,如果不匹配返回null,不然就是一个可复用的fiber节点
function mapRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber,
): Map<string | number, Fiber> {
const existingChildren: Map<string | number, Fiber> = new Map();
let existingChild = currentFirstChild;
while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
return existingChildren;
}
mapRemainingChildren返回一个children构成的Map key为id或者是child的index
内容比较多建议分成三个步骤去看
第一个循环,比较newChildren 和oldFiber和他的兄弟们 会有三种情况
- key不同循环直接停止
- key相同,类型不同,fiber标记为删除循环继续 i++ oldFiber = nextOldFiber;
- 循环结束newChildren或者是oldFiber和他的兄弟们遍历结束了
- 循环完处理一下几种情况 ,一种是newChildren现遍历完了,那删除剩余的oldFiber,deleteRemainingChildren(returnFiber, oldFiber); 第二种是oldFiber遍历完了,那剩余的newChildren 需要创建fiber节点 并且拼接在previousNewFiber这个结果链上 触发这两种情况都会退出整个diff
- 也就是都没有遍历完,情况就是由于节点位置移动导致的,这个时候先要mapRemainingChildren(returnFiber, oldFiber);把剩余的fiber做一个Map映射,然后newChildren 剩余的节点去Map中查找,重点是placeChild函数
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
// Noop.
return lastPlacedIndex;
}
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
//原本节点的index比当前最近一次替换过的节点的index还小的话标记为移动,且lastPlacedIndex不变
newFiber.effectTag = Placement;
return lastPlacedIndex;
} else {
// This item can stay in place.
//返回原有节点的位置作为新的lastPlacedIndex
return oldIndex;
}
} else {
// This is an insertion.
//newChildren在原本没有 完全是新建的
newFiber.effectTag = Placement;
return lastPlacedIndex;
}
}
举个例子如果 01234要变为12304 假设lastPlacedIndex为0初始开始循环
1 oldIndex为1 oldIndex > lastPlacedIndex 不动 lastPlacedIndex = oldIndex也就是1
2 oldIndex为2 oldIndex > lastPlacedIndex 不动 lastPlacedIndex = oldIndex也就是2
3 oldIndex为3 oldIndex > lastPlacedIndex 不动 lastPlacedIndex = oldIndex也就是3
0 oldIndex为0 oldIndex < lastPlacedIndex 标记移动 lastPlacedIndex 不变还是3
4 oldIndex为4 oldIndex > lastPlacedIndex 不动 lastPlacedIndex 改为4
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。