diff的作用
在React中,diff算法需要与虚拟DOM配合才能发挥出真正的威力。React会使用diff算法计算出虚拟DOM中真正发生变化的部分,并且只会针对该部分进行dom操作,从而避免了对页面进行大面积的更新渲染,减小性能的开销。
React diff算法
在传统的diff算法中复杂度会达到O(n^3),比如说我们页面有1000个元素,那么则需要对比10亿次,效率十分低下,不能满足前端渲染所需要的效率。为了解决这个问题,React中定义了三种策略,在对比时,根据策略只需遍历一次树就可以完成对比,将复杂度降到了O(n):
- tree diff:在两个树对比时,只会比较同一层级的节点,会忽略掉跨层级的操作
- component diff:在对比两个组件时,首先会判断它们两个的类型是否相同,如果不同则不会进一步向下比较,会直接销毁组件,创建新的组件插入。
- element diff:对于同一层级的一组节点,会使用具有唯一性的key来区分是否需要创建,删除,或者是移动。
源码结构
diff的入口函数是reconcileChildren:
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
// current === null,说明是创建,不是更新,调用mountChildFibers函数根据子元素创建Fiber
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 对current树和workInProgress树进行diff算法对比,找出差异部分
workInProgress.child = reconcileChildFibers(
workInProgress, // workInProgress书上的Fiber节点
current.child, // current树上的当前Fiber节点的子节点
nextChildren, // 使用最新数据生成的React element元素
renderLanes, // 渲染的lane优先级集合
);
}
}
可以看到这个函数首先判断workInProgress树上的Fiber节点对应的current树上的Fiber节点是否存在,如果等于null,则不存在,说明是创建,不是更新,然后调用mountChildFibers函数根据调用render函数生成的React elements构建workInprogress树。如果不等于null,则存在,说明是更新,然后会调用reconcileChildFibers函数,对current树和workInProgress树进行diff算法对比,找出差异部分进行更新。
diff算法对比的过程在reconcileChildFibers函数,我们来看一下源码:
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// 处理单个节点
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_PORTAL_TYPE:
...
case REACT_LAZY_TYPE:
...
}
// 处理同一层级多个节点
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
....
}
...
}
这边只贴了比较重要的部分,可以看到会根据render函数新生成的React element的类型来判断是单个节点还是多个节点。
单节点
单个节点的diff过程主要是在reconcileSingleElement函数中,我们先来看单个节点是如何做diff的:
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key; // 获取React element元素上的key属性
let child = currentFirstChild; // current树上的Fiber节点
while (child !== null) {
// current树已经被渲染在屏幕上
// 通过current树上的Fiber节点的key属性与新生成的React element元素上的key属性对比,
// 如果不相等,则会把该节点对应的current树上的Fiber对象添加到父Fiber的deletions属性中
// 并且在flags集合中添加删除标识,然后根据新创建的React element元素创建新的Fiber节点
// 在commit阶段会根据flags集合中是否添加删除标识,去拿出deletions属性中添加的Fiber对象,
// 将Fiber对象对应的旧的dom节点包括它下面所有的子节点全部删除,然后将新的节点插入到页面中
// 如果相等,则会复用之前current树上相对应的fiber,并使用最新的props更新fiber上的pendingProps属性
// 在commit阶段会更新dom节点
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
...
return existing;
}
} else {
// key相等,通过current树上的Fiber节点的elementType属性与新生成的React element元素上的type属性对比,判断类型是否相同
// 如果不相等,则会把该节点对应的current树上的Fiber对象添加到父Fiber的deletions属性中
// 并且在flags集合中添加删除标识,然后根据新创建的React element元素创建新的Fiber节点
// 在commit阶段会根据flags集合中是否添加删除标识,去拿出deletions属性中添加的Fiber对象,
// 将Fiber对象对应的旧的dom节点包括它下面所有的子节点全部删除,然后将新的节点插入到页面中
// 如果相等,则会复用之前current树上相对应的fiber,并使用最新的props更新fiber上的pendingProps属性
// 在commit阶段会更新dom节点
if (
child.elementType === elementType ||
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
(enableLazyElements &&
typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
deleteRemainingChildren(returnFiber, child.sibling);
// 复用之前current树上相对应的fiber,并使用最新的props更新fiber上的pendingProps属性
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
}
}
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 当key或者类型不相等时,会根据新创建的React element元素创建新的Fiber节点
...
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
首先获取新生成的React element元素上的key属性,然后通过current树上的Fiber节点的key属性与React element元素上的key属性对比:
- key不相等:则会调用
deleteChild函数把Fiber对象添加到父Fiber的deletions属性中,并且在flags集合中添加删除标识,会在commit阶段将添加进deletions集合中的Fiber对应的dom以及他下面所有的子节点都删除。
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete];
returnFiber.flags |= ChildDeletion;
} else {
deletions.push(childToDelete);
}
}
然后根据新创建的React element元素创建新的Fiber节点。
- key相等:则会通过current树上的Fiber节点的elementType属性与React element元素上的type属性对比,判断类型是否相同:类型相同则会调用
useFiber函数,重用旧的Fiber节点,并使用React element元素上的props更新fiber上的pendingProps属性,可以理解为重用节点,并且会更新该节点的属性,最后返回该节点添加到workInProgress树上。类型不同,则会调用deleteRemainingChildren函数,把Fiber对象添加到父Fiber的deletions属性中,并且在flags集合中添加删除标识,会在commit阶段将添加进deletions集合中的Fiber对应的dom以及他下面所有的子节点都删除。最后根据新创建的React element元素创建新的Fiber节点返回。
小结
单个节点的对比,这个节点可以是组件,也可以是html标签,它们首先都会进行key属性的对比,一般情况下,不是列表中的元素,我们是不加key的,所以key值都为null。在对比的时候,key值都为null,所以会直接对比类型是否相同。如果相同类型,则会更新重用旧节点,如果类型不相同,则会删除旧节点以及它下面所有的子节点,然后根据React element创建新的节点插入。
多个节点
分析完单个节点,我们再来看多个节点的diff,多个节点的diff过程主要是在reconcileChildrenArray函数中实现的:
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
let resultingFirstChild: Fiber | null = null; // 列表中使用React element数据更新了属性的第一个Fiber节点
let previousNewFiber: Fiber | null = null; // 上一个更新了属性的Fiber节点,用以列表中兄弟节点的相互关联
let oldFiber = currentFirstChild; // current树上的列表中的第一个Fiber节点
let lastPlacedIndex = 0; // 上一个元素移动位置的下标
let newIdx = 0; // 遍历React element树的下标
let nextOldFiber = null; // current树上的列表中元素的兄弟节点
// 这个for循环的作用是剔除没有变化的节点,并对节点进行更新和重用
// 当检查到尾部有新增节点时,oldFiber为null,则会跳出循环,然后创建新的Fiber插入到尾部,不需要与其它节点进行对比
// 当有新节点插在中间插入,newFiber则为null,则会跳出循环,然后需要移动节点位置进行排序
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// 判断旧Fiber节点上的key与React element上的key属性相等的话,则会使用React element的数据更新Fiber节点上的属性
// 不相等,则会返回null
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
// 发现有元素的key属性有变化,说明不是更新场景,则会跳出for循环
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
// 将newIdx赋值给workInProgress树上的Fiber节点的index属性,代表当前元素在列表中的位置(下标)
// 判断current树上元素的Index是否小于lastPlacedIndex,是则表示该元素需要移动位置,否则表示不需要移动位置
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
// 将更新了属性的兄弟Fiber节点进行关联
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 新的子节点已经遍历完成,如果还有剩下的节点,表示current树上有,但是workInProgress树上没有的节点,需要全部删除
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 节点新增,不需要与旧节点对比,直接创建新增
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
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;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// 将current树上的列表中还未对比的元素添加进Map对象中
// 下面的for循环会根据key取出Map中对应的旧的Fiber与React element做类型的比较
// 如果类型相同则更新Fiber属性,不同,则会根据React element重新创建一个新的Fiber做插入操作
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 节点移动
for (; newIdx < newChildren.length; newIdx++) {
// 根据key取出Map中对应的旧的Fiber与React element做类型的比较
// 如果类型相同则使用React element的数据更新Fiber节点上的属性进行重用,不同,则会根据React element的数据重新创建一个新的Fiber做插入操作
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
// newFiber.alternate不为null,表示是重用的节点,需要将existingChildren中重用的节点删除掉
// 遍历结束后existingChildren中剩下的节点,则是需要删除的
if (newFiber.alternate !== null) {
// 在调用updateFromMap方法时,会根据key取出相对应的Fiber
// 调用updateFromMap方法完成后,对应key的Fiber值被重用了,所以需要删除Map中使用过的key对应的值
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
// 将newIdx赋值给workInProgress树上的Fiber节点的index属性,代表当前元素在列表中的位置(下标)
// 判断current树上元素的Index是否小于lastPlacedIndex,是则表示该元素需要移动位置,否则表示不需要移动位置
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// 节点删除
if (shouldTrackSideEffects) {
// existingChildren中剩下的Fiber,表示current树上存在,但是workInProgress树上不存在的元素
// 将剩下的Fiber添加到父Fiber节点的deletions属性中, 并且在flags集合中添加删除标识,在commit阶段会将这些元素进行删除
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
// 返回列表中的第一个节点
return resultingFirstChild;
}
下面,我们通过举例子的方式,来分析多节点的diff算法。
新增子节点
假如我们现在界面已经渲染好了,所对应的current树是这样:
现在做了一次更新操作,调用render方法生成的React element树是这样:
在列表的尾部新增了key值为E的元素。
在调用reconcileChildrenArray函数时,所传的参数:
returnFiber:workInProgress树上对应A节点的Fiber对象currentFirstChild:current树上的列表中的第一个元素B所对应的Fiber对象newChildren:调用render方法生成的React element树,也就是上一张图中展示的样子。lanes:需要更新的任务的优先级的集合
在reconcileChildrenArray函数中可以看到,首先将列表中的第一个子节点(B)赋值给了oldFiber,初始化了newIdx为0,nextOldFiber为null,将newIdx作为for循环的索引,对newChildren进行遍历。在遍历的过程中,调用了updateSlot函数,这个函数的作用则是判断current树上的Fiber节点与React element上的key属性和类型是否相等,相等的话,则会使用React element的props更新current树上的Fiber节点上的属性,对旧的Fiber进行重用,不相等,则会返回null:
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
const key = oldFiber !== null ? oldFiber.key : null;
...
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
// 判断key是否相等
if (newChild.key === key) {
// 对类型进行对比,类型相同则更新并且重用旧Fiber,不相同则根据React element重新创建一个新的Fiber
return updateElement(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
}
}
case REACT_PORTAL_TYPE: {
...
}
case REACT_LAZY_TYPE: {
...
}
}
...
}
经过对比,current树上的B节点与React element树上的B节点key属性和类型都一样,则会返回旧的Fiber节点,然后调用placeChild函数,这个函数的主要作用就是判断当前节点是否需要移动:
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
newFiber.index = newIndex;
...
const current = newFiber.alternate;
// 判断workInProgress树上的Fiber节点在current树上是否有对应的Fiber节点
// 有的话则会对比新老Fiber的index,来判断是否需要移动
// 如果current为null,则说明current树上没有对应的Fiber,该Fiber是新增的需要插入
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
// This item can stay in place.
return oldIndex;
}
} else {
// This is an insertion.
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}
这个函数中通过获取current树上的Fiber节点的index属性,也就是它在列表中的位置,与lastPlacedIndex对比,此时lastPlacedIndex为0,B节点在current树中的位置也是0,所以位置不变,只有当节点在current树中的位置小于lastPlacedIndex时,才会进行向右移动。
B对比完成,则开始对D和C依次进行对比,由于D和C都没有变动,所以过程和B一样。
在D开始对比时,会获取它的兄弟节点赋值给nextOldFiber,由于在current树上,D是最后一个节点所以会将null赋值给nextOldFiber:
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
...
oldFiber = nextOldFiber;
}
在D完成对比时,又将nextOldFiber赋值给了oldFiber,在对E开始遍历时,oldFiber为null,则会终止循环,此时的索引newIdx为D的下标:2:
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
...
}
此时,新生成的Reat元素还没有遍历完,还剩一个E,则会进入节点新增的代码中:
// 节点新增,不需要与旧节点对比,直接创建新增
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
根据上面我们知道此时的oldFiber为null,直接进入for循环,调用createChild函数,这个函数的作用,则是根据React element创建一个新的Fiber,然后调用placeChild函数,由于newFiber是新创建的,所以current为null,需要添加插入标识,进行插入操作:
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
...
const current = newFiber.alternate;
// 判断workInProgress树上的Fiber节点在current树上是否有对应的Fiber节点
// 有的话则会对比新老Fiber的index,来判断是否需要移动
// 如果current为null,则说明current树上没有对应的Fiber,该Fiber是新增的需要插入
if (current !== null) {
...
} else {
// 添加插入标识
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}
最后返回resultingFirstChild,也就是对比完成后的列表中的第一个子节点,添加到WorkInProgress树中,形成新的WorkInProgress树。
在这个过程中,React通过diff查找,将key与类型相同的节点都进行了更新和重用(B, C D),将不同的节点进行了创建与插入的操作。
删除子节点
这个例子中我们将C进行了删除。
对比B也是与上面的一样过程,最终结果也是被重用。
当使用D做对比时,调用updateSlot时,传入的参数为:
const newFiber = updateSlot(
returnFiber, // A
oldFiber, // C
newChildren[newIdx], // D
lanes,
);
将D与C进行比较,key不同,updateSlot返回null,然后跳出当前for循环,此时oldFiber为C所对应的Fiber节点,newIdx为1,然后会调用mapRemainingChildren函数,将current树上的列表中还未对比完成的节点C,D添加进Map对象中:existingChildren:
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
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;
}
然后根据newIdx对React element树进行新一轮的遍历:
// 节点移动
for (; newIdx < newChildren.length; newIdx++) {
// 根据key取出Map中对应的旧的Fiber与React element做类型的比较
// 如果类型相同则使用React element的数据更新Fiber节点上的属性进行重用,不同,则会根据React element的数据重新创建一个新的Fiber做插入操作
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
// newFiber.alternate不为null,表示是重用的节点,需要将existingChildren中重用的节点删除掉
// 遍历结束后existingChildren中剩下的节点,则是需要删除的
if (newFiber.alternate !== null) {
// 在调用updateFromMap方法时,会根据key取出相对应的Fiber
// 调用updateFromMap方法完成后,对应key的Fiber值被重用了,所以需要删除Map中使用过的key对应的值
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
// 将newIdx赋值给workInProgress树上的Fiber节点的index属性,代表当前元素在列表中的位置(下标)
// 判断current树上元素的Index是否小于lastPlacedIndex,是则表示该元素需要移动位置,否则表示不需要移动位置
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
进入for循环,此时的newIdx为1,调用updateFromMap函数:
const newFiber = updateFromMap(
existingChildren, // Map: {C: FiberC, D: FiberD}
returnFiber, // A
newIdx, // 1
newChildren[newIdx], // React element D
lanes,
);
function updateFromMap(
existingChildren: Map<string | number, Fiber>,
returnFiber: Fiber,
newIdx: number,
newChild: any,
lanes: Lanes,
): Fiber | null {
...
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
// 根据key取出Map中对应的旧的Fiber与React element做类型的比较
// 如果类型相同则使用React element的数据更新Fiber节点上的属性进行重用,不同,则会根据React element的数据重新创建一个新的Fiber做插入操作
const matchedFiber =
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
return updateElement(returnFiber, matchedFiber, newChild, lanes);
}
case REACT_PORTAL_TYPE: {
...
case REACT_LAZY_TYPE:
...
}
...
}
这个函数的作用则是根据key取出Map中对应的旧的Fiber与React element做类型的比较,如果类型相同则使用React element的数据更新Fiber节点上的属性进行重用,不同,则会根据React element的数据重新创建一个新的Fiber返回。
由于D在existingChildren中是存在的,并且类型也没有变化,所有会重用旧Fiber。
当updateFromMap调用完成,则会判断newFiber是否是重用的,如果是重用的,那么它的alternate属性肯定是不为null的,则会把existingChildren中重用过的Fiber删除。也就是会把D从existingChildren中删除。
然后调用placeChild:
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
newFiber则是D对应current树上的Fiber对象,lastPlacedIndex则是B的位置:0,newIdx为:1:
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
// 将当前位置交给newFiber占据
newFiber.index = newIndex;
const current = newFiber.alternate;
// 判断workInProgress树上的Fiber节点在current树上是否有对应的Fiber节点
// 有的话则会对比新老Fiber的index,来判断是否需要移动
// 如果current为null,则说明current树上没有对应的Fiber,该Fiber是新增的需要插入
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
// This item can stay in place.
return oldIndex;
}
} else {
...
}
}
由于D是重用的,所以current不为null,然后取出D在curren树上的位置:2,赋值给oldIndex,上面提到lastPlacedIndex是B的位置:0,oldIndex大于lastPlacedIndex,不需要移动。
placeChild调用完成后,此时,新的子节点已经遍历完成。
existingChildren中还剩下一个C,到最后existingChildren中剩下的Fiber,表示current树上存在,但是workInProgress树上不存在的节点,需要进行删除,会调用deleteChild函数将剩下的Fiber添加到父Fiber节点的deletions属性中, 并且在flags集合中添加删除标识,在commit阶段会将这些节点进行删除。
移动子节点
在这个例子中,我们将C移动到了D的后面。
首先对比两棵树上的B节点,key和类型都是一样的,对B进行重用。
然后拿D与C进行对比,发现key不同,跳出第一个循环,将未完成对比的节点添加到existingChildren中:C,D。
接着进入第二个循环,调用updateFromMap,根据React element树上D的key值从existingChildren中取出old fiber,然后再将old fiber D的类型与React element中D的类型进行对比,发现是相同的,对old fiber D进行重用,重用完成后从existingChildren中删除D。
然后调用placeChild方法,进行位置移动:
lastPlacedIndex = placeChild(
newFiber, // D节点被重用,为old fiber
lastPlacedIndex, // 0
newIdx // 1
);
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
newFiber.index = newIndex;
...
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
// This item can stay in place.
return oldIndex;
}
...
}
old fiber D的index为current树上的位置为2,lastPlacedIndex为0,oldIndex大于lastPlacedIndex,不进行移动。
拿C与D对比,调用updateFromMap,根据React element树上D的key值从existingChildren中取出old fiber,然后再将old fiber C的类型与React element中D的类型进行对比,发现是相同的,对old fiber C进行重用,重用完成后从existingChildren中删除C。
调用placeChild,old fiber C的index为current树上的位置为1,lastPlacedIndex为2,oldIndex小于lastPlacedIndex,向右移动。
为什么是向右移动?可以看到placeChild中的第一句代码:
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
newFiber.index = newIndex;
...
}
将当前newIndex赋值给了fiber C的index属性,newIndex此时为2,current树上fiber C的index为1,将fiber C向右移动一位index加1,index则变为了2。
从上面我们看到,移动节点的关键在于placeChild函数中的这段代码:
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
return oldIndex;
}
如果我们把D节点移动到第一位呢?
第一次遍历对比,newIndex为0,D在current树上的位置为2,lastPlacedIndex为0,由于oldIndex大于lastPlacedIndex,D节点不会进行移动,index被赋值为0,返回oldIndex赋值给lastPlacedIndex为2。
第二次遍历对比,newIndex为1,B在current树上的位置为0,lastPlacedIndex为2,由于oldIndex小于lastPlacedIndex,B节点向右移动,index被赋值为1,返回lastPlacedIndex为2。
第三次遍历对比,newIndex为2,C在current树上的位置为1,lastPlacedIndex为2,由于oldIndex小于lastPlacedIndex,C节点向右移动,index被赋值为2,返回lastPlacedIndex为2。
可以看到,将D移动到第一位,B,C都会向右移动
一旦节点过多,这样的操作会引起较大的性能开销,所以我们尽可能的避免将尾部的节点向前移动。
总结
- React diff通过三个策略将传统的diff算法的复杂度从O(n^3)降至O(n)。
- tree diff: 在对比两颗树时,只会对比同一层的节点,会忽略跨层级的操作。
- component diff:在对比两个组件时,只对类型进行比较,如果类型不一样,则不会再进一步的比较,会对老的组件及其子节点全部进行销毁,将新的组件创建并插入。
- element diff:对于同一层的一组节点,会使用具有唯一性的key进行区分。
- 不管是单个节点还是多个节点,都会先进行key值的比较,然后再进行类型的比较,以此判断是否需要对该节点进行重用,还是创建。
- 关于key属性的作用,我们可以看到每次遍历都会先进行key值的对比,通过key可以快速的找到变化的节点,并针对这些节点进行操作。
- 在开发中,我们尽量不要将尾部的节点向前移动,能够减小性能的开销。