对于
update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点。
diff算法的On3怎么来的
在react架构中,我提到上一次渲染的Fiber也就是已经呈现在页面中的结点是current fiber,本次更新是workinprogress fiber。我们都知道react的diff算法是从O(n3)的时间复杂度变成了O(n),这里说一下O(n3)是怎么来的:
- 将两颗树中所有的节点一一对比需要O(n²)的复杂度,
- 在对比过程中发现旧节点在新的树中未找到,那么就需要把旧节点删除,删除一棵树的一个节点(找到一个合适的节点放到被删除的位置)的时间复杂度为O(n),同理添加新节点的复杂度也是O(n),合起来diff两个树的复杂度就是O(n³)
diff算法的优化
为了降低算法复杂度,React的diff会预设三个限制:
- 只对同级元素进行
Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。 - 两个不同类型的元素会产生出不同的树。如果元素由
div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。 - 开发者可以通过
key prop来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:
diff算法
我们从Diff的入口函数reconcileChildFibers出发,该函数会根据newChild(即JSX对象)类型调用不同的处理函数。
// 根据newChild类型选择不同diff函数处理
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
): Fiber | null {
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
// object类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// 调用 reconcileSingleElement 处理
// // ...省略其他case
}
}
if (typeof newChild === 'string' || typeof newChild === 'number') {
// 调用 reconcileSingleTextNode 处理
// ...省略
}
if (isArray(newChild)) {
// 调用 reconcileChildrenArray 处理
// ...省略
}
// 一些其他情况调用处理函数
// ...省略
// 以上都没有命中,删除节点
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
我们可以从同级的节点数量将Diff分为两类:
- 当
newChild类型为object、number、string,代表同级只有一个节点 - 当
newChild类型为Array,同级有多个节点。
单节点的diff
单个节点的diff会进入reconcileSingleElement方法,这个方法主要做的事情其实是
我们可以大致的看看源码中是怎么实现的
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 判断存在的dom结点
while (child !== null) {
// 判断上一次的dom结点是否可以复用
// 比较key
if (child.key === key) {
switch (child.tag) {
case Fragment: {
if (element.type === REACT_FRAGMENT_TYPE) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
}
break;
}
case Block:
if (enableBlocksAPI) {
let type = element.type;
if (type.$$typeof === REACT_LAZY_TYPE) {
type = resolveLazyType(type);
}
if (type.$$typeof === REACT_BLOCK_TYPE) {
// The new Block might not be initialized yet. We need to initialize
// it in case initializing it turns out it would match.
if (
((type: any): BlockComponent<any, any>)._render ===
(child.type: BlockComponent<any, any>)._render
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.type = type;
existing.return = returnFiber;
return existing;
}
}
}
default: {
if (child.elementType === element.type)
// key相同同事type也相同,表示可以复用,返回复用的fiber
{
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
}
break;
}
}
// key相同type不同,将该fiber和兄弟fiber标记为删除
deleteRemainingChildren(returnFiber, child);
break;
} else {
// key不同,标记删除
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 创建新的fiber
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
从代码中,我们可以看出React首先会判断key是否相同,key不相同直接进入删除逻辑,key相同:
- type相同:可以进行复用
- type不同:执行
deleteRemainingChildren将child及其兄弟fiber都标记删除。
多结点的diff
如果是多个结点的diff,他的children属性是一个包含多个节点的数组。那么reconcileChildFibers的newChild参数类型为Array,在reconcileChildFibers函数内部对应如下情况
// 多个节点之间的diff
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
多节点的diff情况比较复杂,分为如下几种情况:
- 节点更新
- 节点的属性的变化
- 节点的类型的变化
- 节点的增删
- 节点的位置的变化
注意:
在我们做数组相关的算法题时,经常使用双指针从数组头和尾同时遍历以提高效率,但是这里却不行。
虽然本次更新的
JSX对象newChildren为数组形式,但是和newChildren中每个组件进行比较的是current fiber,同级的Fiber节点是由sibling指针链接形成的单链表,即不支持双指针遍历。即
newChildren[0]与fiber比较,newChildren[1]与fiber.sibling比较。所以无法使用双指针优化
因为在开发中大部分都是结点的更新操作,所以React会优先处理节点的更新操作。
第一轮遍历
首先让newChildren[i]与oldFiber对比,然后让i++、nextOldFiber = oldFiber.sibling。在第一轮遍历中,会处理三种情况,其中第1,2两种情况会结束第一次循环
- key不同,第一次循环结束
- newChildren或者oldFiber遍历完,第一次循环结束
- key同type不同,标记oldFiber为DELETION
- key相同type相同则可以复用
newChildren遍历完,oldFiber没遍历完,在第一次遍历完成之后将oldFiber中没遍历完的节点标记为DELETION,即删除的DELETION Tag
第一轮遍历的代码:
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber; // nextOldFiber赋值
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling; // nextOldFiber赋值oldFiber的兄弟节点
}
const newFiber = updateSlot( //更新结点如果key不同则newFiber = null
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break; //跳出第一次遍历
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// 匹配了 slot,但没有重用现有的 Fiber,删除现有的 child。
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); //标记插入节点的位置
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
第二轮遍历
- newChildren和oldFiber都遍历完:多节点diff过程结束
- newChildren没遍历完,oldFiber遍历完,将剩下的newChildren的节点标记为Placement,即插入的Tag
- newChildren和oldFiber没遍历完,则进入节点移动的逻辑
第二轮遍历源码:
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;// 返回diff之后的第一个结点
}
第三轮遍历
第三轮遍历的主要逻辑,在placeChild函数中,例如更新前节点顺序是ABCD,更新后是ACDB,假设type都相同
- newChildren中第一个位置的A和oldFiber第一个位置的A,key相同可复用,lastPlacedIndex=0。
- newChildren中第二个位置的C和oldFiber第二个位置的B,key不同跳出第一次循环,将oldFiber中的BCD保存在map中
- 继续遍历newChildren,newChildren中第二个位置的C在oldFiber中的index=2 > lastPlacedIndex=0不需要移动,lastPlacedIndex=2
- newChildren中第三个位置的D在oldFiber中的index=3 > lastPlacedIndex=2不需要移动,lastPlacedIndex=3
- newChildren中第四个位置的B在oldFiber中的index=1 < lastPlacedIndex=3,移动到最后
我们再来看一个例子:
- 第一次遍历,key不相同,我们把整个oldFiber保存在map中,lastPlacedIndex=0
- 继续遍历newChildren,第一个节点D在oldFiber中存在,index = 3 > lastPlacedIndex = 0,不需要移动
- 第二个节点A在oldFiber结点中存在,index = 0 < lastPlacedIndex = 3,移动到最后
- 第三个节点B在oldFiber结点中存在,index = 1 < lastPlacedIndex = 3,移动到最后
- 第四个节点C在oldFiber结点中存在,index = 2 < lastPlacedIndex = 3,移动到最后
这里不得不提一嘴,就是要尽量减少将节点从后面移动到前面的操作。从上例子我们可以看出,我们从ABCD变成DABC,并不是把D移动到最前面,而是将D不动,ABC移动到最后面。
第三次遍历的源码:
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap( //从map中获取到fiber
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// 新的fiber是一个workinprogress,但是这儿存在一个current,
// 我们要复用这个fiber就需要从childlist中删除,而不是添加deletionlist
existingChildren.delete( //找到删除的结点
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
// 标记为插入逻辑,得到lastPlacedIndex
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
整个过程源码:
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) {
// oldIndex < lastPlacedIndex 将结点插入到后面
newFiber.flags = Placement;
return lastPlacedIndex;
} else {
//不需要移动
return oldIndex;
}
} else {
// 这是新增插入
newFiber.flags = Placement;
return lastPlacedIndex;
}
}
function reconcileChildrenArray(
returnFiber: Fiber, // 父fiber结点
currentFirstChild: Fiber | null, //childs中第一个节点
newChildren: Array<*>, // 新结点数组
lanes: Lanes, // 优先级
): Fiber | null {
let resultingFirstChild: Fiber | null = null; //diff之后返回的第一个节点
let previousNewFiber: Fiber | null = null; // 新节点中上次对比过的结点
let oldFiber = currentFirstChild; //正在对比的oldFiber
let lastPlacedIndex = 0; //上次可以复用的结点的位置或者oldFiber的位置
let newIdx = 0; //新结点中对比到了的位置
let nextOldFiber = null; //正在对比的oldFiber
// 开始第一轮遍历
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber; // nextOldFiber赋值
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling; // nextOldFiber赋值oldFiber的兄弟节点
}
const newFiber = updateSlot( //更新结点如果key不同则newFiber = null
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// 匹配了 slot,但没有重用现有的 Fiber,删除现有的 child。
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); //标记插入节点的位置
if (previousNewFiber === null) {
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) {
//第二次遍历,处理新增节点
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);//创建新的节点
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); //插入新增结点
if (previousNewFiber === null) { // 返回diff之后的第一个结点
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// 将剩下的oldFiber加入map中
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 第三次循环 处理节点移动
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap( //从map中获取到fiber
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// 新的fiber是一个workinprogress,但是这儿存在一个current,
// 我们要复用这个fiber就需要从childlist中删除,而不是添加deletionlist
existingChildren.delete( //找到删除的结点
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
// 标记为插入逻辑,得到lastPlacedIndex
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// 循环结束删除existingChildren中剩下的节点
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}