react element 级 diff 代码详解
话不多说,直接进入正题
重点文件:
ClassComponent 最终在 updateClassComponent(位于 ReactFiberBeginWork) 执行 render 函数,并且获取 children;
FunctionComponent
获取 children
diff 算法就是用于 diff 前后两次 children 之间的差异的,那么第一步就是要获取 children;
FunctionComponent 最终在 renderWithHooks(位于 ReactFiberBeginWork) 中执行,并且获取 children;
reconcileChildren 调和 children
获取 children 后将会进入 reconchileChildren(位于 ReactFiberBeginWork) 函数;
在该函数内,将会根据情况进入不同的函数;如果是第一次渲染,Fiber 树还没有挂载到浏览器中,那么将会执行 mountChildFibers;否则将会执行 reconcileChildFibers。
其实这两个函数都是 ChildReconciler(位于ReactChildFiber) 函数传入不同参数的返回值;
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
这里传入的 true 和 false 在 ChildReconciler 中的参数名字是 shouldTrackSideEffects 意思是:是否应该跟踪副作用,后续还会说到;
如果 children 是 Array,那么最终就会进入发生 diff 的 reconcileChildrenArray(位于 ReactChildFiber) 中;
为什么要 children 是 Array 呢?在 React 中针对与不是 Array 也就是 singleChild 的情况将在 reconcileSingleElement(位于 ReactChildFiber, 这个函数只处理 child 是 ReactElement 的情况) 中进行处理;这个函数中的策略是:不重用已有的 Fiber 直接创建新的 Fiber;
重点:进入 reconcileChildrenArray 开始 diff
用于测试的代码
const FunctionDiff = () => {
const [list, setList] = React.useState([1, "hidden", 3, 4, "hidden", 6]);
React.useEffect(() => {
setTimeout(() => {
setList([1, 2, 4, 3, 5, 6]);
}, 2000);
}, []);
return (
<div>
{list.map((item) => {
if (item === "hidden") return false;
if (item === 4) return <p key={item}>{item}</p>;
return <span key={item}>{item}</span>;
})}
</div>
);
};
直接看代码中的注释吧~
代码稍微精简了一下,不然太长了;
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
// 因为 Fiber.child 就是指的第一个 child 所以需要使用一个变量存储 FirstChild
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
// 这里的 currentFirstChild 就是指挂载在 DOM 上哪一部分老的 Fiber.child
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;// 这个索引指向:最后一个 复用 fiber,且复用其位置不用重新 placement 的fiber;这样说可能比较难以理解;可以详细看看 placeChild 函数,和最下面的例子的解释
let newIdx = 0;
let nextOldFiber = null;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// !注意,oldFiber 中将会存储真实的 index, 比如 children 为: [h1, null, false, Icon, p] 其中 null, false 都无效,所以实际渲染的只有 h1, Icon, p 但是这三个的 Fiber 中将会记录真实的 index; h1.index = 0; Icon.index = 3, p.index = 4;
// *所以如果这里的 index 对不上了,那么说明插入了新节点:[h1, null, span] -> [h1, p, span] 当第二次遍历时:此时的 oldFiber = span; oldFiber.index = 2; 但是 newIndex = 1, 也就是 p; 所以 oldFiber.index > newIdx 是插入了新节点;
// *至于为什么 第二次遍历是 oldFiber = span;因为使用的是 oldFiber.sibling; 移动指针的; null 没有生成 Fiber;
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
// !newFiber 为空时,绝大部分情况都是 key 前后不一样 或者 newChild 本身不存在(null, false..)
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {// *注意看上面,oldFiber.index > newIdx 时会对 oldFiber 赋值为 null 也就是说,当前的情况是中间插入了新的节点
oldFiber = nextOldFiber;// !为什么要给 oldFiber 赋值为 nextOldFiber 呢?假设 [span, false, p2] -> [span, p1, p2] 这个时候,第一次循环时是正常的,所以 nextOldFiber = p2; 第二次循环时,因为 p2.index 与 p1.index 对不上号,所以 oldFiber 被赋值为 null; newFiber 也是 null; 但是我们不能让链表就这么断了呀, p2 及其后面的 fiber 还是有可复用的可能性的,恰巧 nextOldFiber 记录了 p2,所以赋值给 oldFiber;
}
break;
}
if (shouldTrackSideEffects) {// 更新时的 shouldTrackSideEffects 为 true
if (oldFiber && newFiber.alternate === null) {// !如果复用了 oldFiber 那么 newFiber.alternate 应该指向 oldFiber 但是如果进入这个 if 那么说明没有复用 oldFiber;目前我已知的情况是 elementType 发生了变化,比如说 div -> p 但是 key 一致,就会出现这样的问题;那么删除现有的 child 是合理的
// We matched the slot, but we didn't reuse the existing fiber, so we need to delete the existing child.
// *翻译:我们匹配了 slot,但是没有复用已存在的 fiber 所以我们需要删除已存在的 child
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);// *newIdx 在循环中并未被重新赋值
if (previousNewFiber === null) {// 这个很好理解吧
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;// 构建新的fiber链表
}
previousNewFiber = newFiber;// 通通指向下一个
oldFiber = nextOldFiber;// 通通指向下一个
}
/***
* for 循环到这里就结束了,想一想 for 循环结束时是些什么情况
* *1. 正常结束循环
* 1) oldFiber 为 null, newChildren 还未遍历完成: 说明在后面增加了新节点
* 2) newChildren 遍历完成, oldFiber 不为 null: 说明删除了最后面一部分节点
* 3) oldFiber 为 null && newChildren 遍历完成: 说明没有增加或者删除节点
* *2. break 结束循环 也就是 newFiber 为 null
* 1) oldFiber 不为 null: 这一块需要具体分析 updateSlot 中的代码,注意并未分析 React.lazy
* 1: key 不一样 直接返回 null,也就是节点 key 发生变化
* 2: newChild 本身不存在(null, false, undefined...)
* 3: 其他罕见情况(不确定有没有这种情况)
* 2) oldFiber 为 null:在 oldFiber 之前插入了新节点
*
* 在👇就是处理这些情况
*/
// *对标 1.2) | 1.3) 情况
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);// *newChildren 遍历完了,删除多余的 oldFiber
return resultingFirstChild;
}
// *对标 1.1) 情况
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++) {// *循环新增的节点,创建 fiber
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;
}
// 👇 对标所有情况
// Add all children to a key map for quick lookups.
// *将 oldFiber 及其后续 (oldFiber.sibling...) 全部存放到 map 中去,以大致结构为 Mao<fiber.key, fiber> 如果 oldFiber 为 null 就为空 map
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {
// *与 updateSlot 类似,但是 oldFiber 来自于 map 中;(注意:如果 newChild.key 不存在,那么将使用 newChild.index 作为 newIdx 进行查询,所以这里传入了 newIdx)
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
// *只有 newChild 本身不存在,或者罕见情况下会返回 null
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
// *放置 child
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.
// *删除剩余的 oldFiber
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// Update the fiber if the keys match, otherwise return null.
// 翻译:如果 key 匹配,那么就更新对应的 Fiber,否则就返回 null
const key = oldFiber !== null ? oldFiber.key : null;
if (typeof newChild === 'string' || typeof newChild === 'number') {// 如果 child 是 string 或者 number
if (key !== null) {// 文本节点是没有 key 的,但是 oldFiber.key 有值就说明更新前后是不匹配的,一律看作其他操作(比如添加,删除操作)不应该进行更新
return null;
}
return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
if (newChild.type === REACT_FRAGMENT_TYPE) return updateFragment(...);
return updateElement(returnFiber, oldFiber, newChild, lanes);// *如果 oldFiber 不存在,那么就直接创建新 fiber,如果 newChild 的 type 没有变化,那么就复用 oldFiber 的部分内容
} else return null;
}
case REACT_PORTAL_TYPE: {
if (newChild.key === key) {
return updatePortal(returnFiber, oldFiber, newChild, lanes);
} else return null;
}
case REACT_LAZY_TYPE: { ... }
// *如果是一个 数组 大概这个意思 [h1, [p, p, p], div] 中的 [p, p, p]
if (isArray(newChild) || getIteratorFn(newChild)) {
if (key !== null) {// *同理,数组应该也没有 key,如果 oldFiber 有 key,那么说明前后不匹配不应该进行更新
return null;
}
// *否则就使用 Fragment 相当于 React 帮我们加了一个 Fragment
return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
}
}
// *如果上面的都不匹配的话,那么就返回 null
return null;
}
/**
* @desc 如果 current 不存在,那么就直接创建新 Fiber;如果 current 存在且 type 一致,那么就直接复用 fiber 部分内容
* */
function updateElement(
returnFiber: Fiber,
current: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
if (current !== null) {
if (current.elementType === element.type) {
const existing = ...;// 复用 fiber
return existing;
}
}
// Insert 当 elementType 不一样时将会进入这里
const created = ...;// 重新创建 fiber
return created;
}
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;
}
function updateFromMap(
existingChildren: Map<string | number, Fiber>,
returnFiber: Fiber,
newIdx: number,
newChild: any,
lanes: Lanes,
): Fiber | null {
if (typeof newChild === 'string' || typeof newChild === 'number') {
const matchedFiber = existingChildren.get(newIdx) || null;
return updateTextNode(returnFiber, matchedFiber, '' + newChild, lanes);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
const matchedFiber =
existingChildren.get(newChild.key === null ? newIdx : newChild.key) || null;
if (newChild.type === REACT_FRAGMENT_TYPE) return updateFragment(...);
return updateElement(returnFiber, matchedFiber, newChild, lanes);
}
case REACT_PORTAL_TYPE: {
const matchedFiber =
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
return updatePortal(returnFiber, matchedFiber, newChild, lanes);
}
case REACT_LAZY_TYPE:
if (enableLazyElements) {...}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
const matchedFiber = existingChildren.get(newIdx) || null;
return updateFragment(returnFiber, matchedFiber, newChild, lanes, null);
}
}
return null;
}
[span1] -> [span1, span2]:正常结束没啥说的,循环结束后,又接着循环 [span2],为新增的节点创建 Fiber 并且打上标记
[span1, span2] -> [span1]:也是正常结束,循环接受后,删除 oldFiber 多余的部分就是了
[span1, span2] -> [span2, span1]:key不相同,第一次循环时就会 break;然后就开始生成 map 了:map{'1'->span1, '2'->span2}; 然后开始循环;第一次循环:span2 将会获取 oldSpan2Fiber 所以将会复用 oldSpan2Fiber (即使map中没有对应的,也就是获取到的为 null 那么也会为其创建新的 fiber,只是不复用了而已);第二次循环,复用 oldSpan1Fiber ;
[span1, span2] -> [span1, span3, span2]:第一次循环,span1 没有发生变化,一切正常;第二次循环,span3 与 span2 因为 key 值不相同,所以直接break,然后创建 map;然后循环 [span3, span2] 。。。
[span1, span2] -> [span1, p2, span3]: 第一次循环,一切正常;第二次循环,因为 key 相等,所以直接 updateElement,但是因为两者的 elementType 不同,所以为 p2 创建新的 fiber;然后因为 oldFiber 存在,但是 p2Fiber.alternate 为 null 所以将直接删除 span2Fiber. 第三次循环,因为 oldFiber 为 null 了,所以进入下面的循环开始循环 [span3] 并为其创建新的 fiber;
[span1, span2, span3] -> [span1, span3]: 第一次循环,一切正常,第二次循环,span3 与 span2 key 不一样,所以直接 break;然后创建 map,循环[span3], 从 map 中复用 oldFiber,然后删除多余的 fiber 也就是 span2;
[{index: 1, key: 1}, {index: 2, key: 2}] -> [{index: 2, key: 1}, {index: 1, key: 2}] 标签都为 p:第一次循环,因为 key 相同,标签相同,直接复用;但是在后面的 placeChild 函数中,oldIdx = 1, newIdx = 2;说明还是发生了移动,那么就要给这个 fiber 打上 Placement 标记;因为还不是太懂 flag 和 Placement 所以暂时不去调查 lastPlacedIndex 的具体作用;第二次循环,因为 key 相同,标签相同,直接进入 placeChild 这次的 oldIdx = 2, newIdx = 1, 所以直接返回 oldIdx;
TODO
- 将注释分离出来,代码切片进行讲解
- 示例做成图例