React diff 算法的流程
React diff 算法的流程如下:
- 比较两棵树的根节点,如果不同,则认为整棵树需要更新。
- 对于相同的节点,比较它们的属性和子节点。
- 对于同级节点,可以通过唯一 key 标识来判断是否为同一个节点,从而避免不必要的更新。
- 递归处理所有子节点。
- 对于有 key 的子节点,React 会尝试在旧的子节点中查找是否存在与之对应的节点。如果找到了,则将新节点复用旧节点,并对其进行更新;如果没有找到,则将新节点插入到相应位置。
- 对于新增的节点,直接插入到相应位置。
- 对于旧节点中已经不存在的节点,直接删除。
- 在比较过程中,React 会尽可能地复用已有的节点,以最小化 DOM 操作的次数。同时,React 还提供了一些优化手段,如 shouldComponentUpdate 和 React.memo,让开发者可以在需要时自定义组件更新的逻辑和条件。
整个过程中,React 通过记录每个节点的状态和变化情况,最终生成一组变更操作(比如要插入、删除、更新哪些节点),并一次性批量地执行这些操作,以提高性能。
在实际应用中,React diff 算法还会针对一些特殊情况进行优化。比如:
- 对于同级节点的移动操作,React 会尽可能地减少移动的次数,以提高性能。
- 对于同级列表的渲染,React 会使用
key
属性来判断是否需要更新,从而避免因为顺序变化导致的不必要更新。 - 对于大型列表的渲染,建议采用 “虚拟滚动” 技术,只渲染屏幕内可见的部分,从而提高性能和用户体验。
总的来说,React diff 算法是 React 中非常重要的一部分,它通过智能地判断哪些节点需要更新、哪些节点可以复用等方式,使得 React 应用在处理大量数据时依然能够保持良好的性能表现。
React diff 算法中,单节点和多节点的区别
在 React diff 算法中,单节点和多节点的区别主要体现在更新策略上。
/**
* 比较子Fibers DOM-DIFF 就是用老的子fiber链表和新的虚拟DOM进行比较的过程
* @param {*} returnFiber 新的父Fiber
* @param {*} currentFirstChild 老fiber第一个子fiber current一般来说指的是老
* @param {*} newChild 新的子虚拟DOM h1虚拟DOM
*/
function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
//现在需要处理更新的逻辑了,处理dom diff
//考虑新的节点只有一个的情况
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild));
default:
break;
}
}
//newChild [hello文本节点,span虚拟DOM元素]
if (isArray(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild);
}
return null;
}
return reconcileChildFibers;
}
对于单节点,React diff 算法会直接比较其内容是否有变化,如果有变化,则直接更新该节点的内容,而不会进行进一步的子节点比较。这是因为单节点没有子节点,所以不存在子节点在顺序、位置等方面的变化,因此可以省略掉一些比较和操作。
/**
*
* @param {*} returnFiber 根fiber div#root对应的fiber
* @param {*} currentFirstChild 老的FunctionComponent对应的fiber
* @param {*} element 新的虚拟DOM对象
* @returns 返回新的第一个子fiber
*/
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
//新的虚拟DOM的key,也就是唯一标准
const key = element.key; // null
let child = currentFirstChild; //老的FunctionComponent对应的fiber
while (child !== null) {
//判断此老fiber对应的key和新的虚拟DOM对象的key是否一样 null===null
if (child.key === key) {
//判断老fiber对应的类型和新虚拟DOM元素对应的类型是否相同
if (child.type === element.type) {// p div
deleteRemainingChildren(returnFiber, child.sibling);
//如果key一样,类型也一样,则认为此节点可以复用
const existing = useFiber(child, element.props);
existing.ref = element.ref;
existing.return = returnFiber;
return existing;
} else {
//如果找到一key一样老fiber,但是类型不一样,不能此老fiber,把剩下的全部删除
deleteRemainingChildren(returnFiber, child);
}
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
//因为我们现实的初次挂载,老节点currentFirstChild肯定是没有的,所以可以直接根据虚拟DOM创建新的Fiber节点
const created = createFiberFromElement(element);
created.ref = element.ref;
created.return = returnFiber;
return created;
}
//将删除的节点放入returnFiber.deletions
function deleteChild(returnFiber, childToDelete) {
if (!shouldTrackSideEffects)
return;
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete]
returnFiber.flags |= ChildDeletion;
} else {
returnFiber.deletions.push(childToDelete);
}
}
对于多节点,React diff 算法需要逐层比较其子节点的内容和状态,并根据变化情况采取不同的更新策略。具体来说,React diff 算法会通过比较每个子节点的 “key” 属性来确定它们的唯一性和相对位置,然后尽可能地复用已有的节点,并最小化 DOM 操作的次数。如果出现了新增、删除、移动等操作,则会相应地执行插入、删除、移动等操作,从而实现整个容器节点的更新。
// 第一轮循环 删除没有复用的老节点
// 使用lastPlacedIndex 来判断是否移动节点
//如果找到的老fiber的索引比lastPlacedIndex要小,则老fiber对应的DOM节点需要移动
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
let resultingFirstChild = null; //返回的第一个新儿子
let previousNewFiber = null; //上一个的一个新的儿fiber
let newIdx = 0;//用来遍历新的虚拟DOM的索引
let oldFiber = currentFirstChild;//第一个老fiber
let nextOldFiber = null;//下一个第fiber
let lastPlacedIndex = 0;//上一个不需要移动的老节点的索引
// 开始第一轮循环 如果老fiber有值,新的虚拟DOM也有值
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
//先暂下一个老fiber
nextOldFiber = oldFiber.sibling;
//试图更新或者试图复用老的fiber
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
if (newFiber === null) {
break;
}
if (shouldTrackSideEffects) {
//如果有老fiber,但是新的fiber并没有成功复用老fiber和老的真实DOM,那就删除老fiber,在提交阶段会删除真实DOM
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
//指定新fiber的位置, 确定节点是否可以复用,移动或者替换
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;//li(A).sibling=p(B).sibling=>li(C)
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber
}
//新的虚拟DOM已经循环完毕,3=>2
if (newIdx === newChildren.length) {
//删除剩下的老fiber
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
if (oldFiber === null) {
// 处理完需要删除的节点
//如果老的 fiber已经没有了, 新的虚拟DOM还有,进入插入新节点的逻辑
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);
if (newFiber === null) continue;
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
//如果previousNewFiber为null,说明这是第一个fiber
if (previousNewFiber === null) {
resultingFirstChild = newFiber; //这个newFiber就是大儿子
} else {
//否则说明不是大儿子,就把这个newFiber添加上一个子节点后面
previousNewFiber.sibling = newFiber;
}
//让newFiber成为最后一个或者说上一个子fiber
previousNewFiber = newFiber;
}
}
// 开始处理移动的情况
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
//开始遍历剩下的虚拟DOM子节点
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
//如果要跟踪副作用,并且有老fiber
if (newFiber.alternate !== null) {
existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
}
}
//指定新的fiber存放位置 ,并且给lastPlacedIndex赋值
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber; //这个newFiber就是大儿子
} else {
//否则说明不是大儿子,就把这个newFiber添加上一个子节点后面
previousNewFiber.sibling = newFiber;
}
//让newFiber成为最后一个或者说上一个子fiber
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
//等全部处理完后,删除map中所有剩下的老fiber
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
function placeChild(newFiber, lastPlacedIndex, newIdx) {
//指定新的fiber在新的挂载索引
newFiber.index = newIdx;
//如果不需要跟踪副作用
if (!shouldTrackSideEffects) {
return lastPlacedIndex;
}
//获取它的老fiber
const current = newFiber.alternate;
//如果有,说明这是一个更新的节点,有老的真实DOM。
if (current !== null) {
const oldIndex = current.index;
//如果找到的老fiber的索引比lastPlacedIndex要小,则老fiber对应的DOM节点需要移动
if (oldIndex < lastPlacedIndex) {
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
return oldIndex;
}
} else {//如果没有,说明这是一个新的节点,需要插入
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}
总的来说,单节点和多节点虽然都遵循 React diff 算法的基本流程,但在具体的更新策略上存在一些差异,需要针对不同的场景进行优化处理。
除了更新策略上的差异,单节点和多节点在渲染性能上也有一些区别。####
对于单节点,由于它没有子节点,所以不需要递归调用 diff 算法,因此处理速度很快,渲染性能比较高。
对于多节点,由于它需要逐层比较子节点的状态和位置,并可能涉及到插入、删除、移动等操作,所以需要进行递归调用 diff 算法,并且随着节点数量的增加,处理速度会逐渐变慢。因此,在大规模列表渲染等场景下,需要采取一定的优化措施,如使用虚拟滚动等技术,以提高渲染性能和用户体验。
总的来说,单节点和多节点都是 React 应用中常见的节点类型,在实际应用中需要根据具体情况选择合适的节点类型,并针对不同的场景进行优化处理,以保证应用的渲染性能和用户体验。
diff中lastPlaceIndex的作用
在 React diff 算法中,lastPlaceIndex
(也称为“上一个节点的位置索引”)是一个用于优化节点移动操作的重要参数,它记录了当前正在遍历的子节点列表中,上一个参与比较的节点在原先列表中的位置。
具体来说,当 React diff 算法在比较两个相邻的子节点时,会将上一个子节点的 lastPlaceIndex
与当前子节点的位置进行比较。如果当前子节点的位置小于等于上一个子节点的位置,则认为这两个子节点之间的距离没有变化,不需要进行移动操作;否则,会将当前子节点插入到正确的位置,并将上一个子节点从原先位置删除,以实现节点在列表中的移动操作。
通过使用 lastPlaceIndex
参数,React diff 算法可以快速判断节点的移动情况,并最小化 DOM 操作的次数,从而提高应用的渲染性能。同时,为了避免 lastPlaceIndex
参数过期或失效导致的错误比较结果,React diff 算法会在处理完每个子节点后更新 lastPlaceIndex
的值,以确保其准确性和有效性。
转载自:https://juejin.cn/post/7213695693201260600