react diff 为了减低传统 diff 算法的复杂度,对diff会预设3个限制:
- 只对同级元素进行 diff:如果一个dom节点在前后两次更新中跨越了层级,那么react不会尝试服用他
- 两个不同类型的元素会产生不同的树:如果元素由div变成了p,react会销毁div及其子孙节点,并新建p及其子孙节点
- 通过key来暗示哪些子元素在不同的渲染下能保持稳定
可以分为 单一节点diff 和多节点diff,本质上是比较 currentFiber 和 jsx 对象,以及最终生成 workInProgressFiber
单一节点
js
// Handle object types
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
mergeDebugInfo(debugInfo, newChild._debugInfo),
),
);
// ...
}
// ...
throwOnInvalidObjectType(returnFiber, newChild);
}
js
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
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 {
if (
child.elementType === elementType ||
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
coerceRef(returnFiber, child, existing, element);
existing.return = returnFiber;
return existing;
}
}
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
// 当 key 或者类型不相等时,会根据新创建的 React element 元素创建新的 Fiber 节点
const created = createFiberFromElement(element, returnFiber.mode, lanes);
// 添加 ref 属性 { current: DOM }
created.ref = coerceRef(returnFiber, currentFirstChild, element);
// 添加父级 Fiber 对象
created.return = returnFiber;
// 返回创建好的子Fiber
return created;
}
}
第一次mount时,会直接生成一个fiber节点并返回
js
function reconcileSingleElement(
returnFiber: Fiber, // 父级fiber
currentFirstChild: Fiber | null, // currentfiber,由于是mount时,它为null
element: ReactElement, // jsx 对象
lanes: Lanes, // 优先级
debugInfo: ReactDebugInfo | null,
): Fiber {
// ...
if (element.type === REACT_FRAGMENT_TYPE) {
// ...
} else {
// 当 key 或者类型不相等时,会根据新创建的 React element 元素创建新的 Fiber 节点
const created = createFiberFromElement(element, returnFiber.mode, lanes);
// 添加 ref 属性 { current: DOM }
created.ref = coerceRef(returnFiber, currentFirstChild, element);
// 添加父级 Fiber 对象
created.return = returnFiber;
// 返回创建好的子Fiber
return created;
}
}
如果上次更新存在对应的更新节点,我们就需要判断dom节点是否可以被复用,我们先看看可复用的状态下
js
function reconcileSingleElement(
returnFiber: Fiber, // 父级fiber
currentFirstChild: Fiber | null, // currentfiber,由于是mount时,它为null
element: ReactElement, // jsx 对象
lanes: Lanes, // 优先级
debugInfo: ReactDebugInfo | null,
): Fiber {
const key = element.key; // null
let child = currentFirstChild; // FiberNode<div>
while (child !== null) {
// null === null
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
// ...
} else {
if (
child.elementType === elementType ||
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
// 本次更新是单一节点更新,所以需要他的兄弟节点
deleteRemainingChildren(returnFiber, child.sibling);
// 复用老的fiber,并更新 props,设置index = 0,sibling = null
const existing = useFiber(child, element.props);
coerceRef(returnFiber, child, existing, element);
existing.return = returnFiber;
// 返回复用节点
return existing;
}
}
// ...
break;
} else {
// ...
}
// ...
}
// ...
}
我们现在看看不能复用的逻辑,那么什么情况下不一样呢?
- type 不同
- key 不同
我们先看 key 值不一样
php
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
if (child.key === key) {
// ...
} else {
// 删除当前 fiber 节点
deleteChild(returnFiber, child);
}
// 设置兄弟节点,如果没有兄弟节点则为 null,跳出 while 循环
child = child.sibling;
}
if (element.type === REACT_FRAGMENT_TYPE) {
// ...
} else {
}
}
如果jsx和当前节点key值不一样,则会删除当前节点,继续匹配他的兄弟节点,以此类推,如果都不匹配,则会新建一个节点。
我们看看 type 不一样
js
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
// ...
} else {
if (
child.elementType === elementType ||
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
// ...
}
}
// 删除当前节点及兄弟节点
deleteRemainingChildren(returnFiber, child);
break;
} else {
// ...
}
// ...
}
if (element.type === REACT_FRAGMENT_TYPE) {
// ...
} else {
// 当 key 或者类型不相等时,会根据新创建的 React element 元素创建新的 Fiber 节点
const created = createFiberFromElement(element, returnFiber.mode, lanes);
// 添加 ref 属性 { current: DOM }
created.ref = coerceRef(returnFiber, currentFirstChild, element);
// 添加父级 Fiber 对象
created.return = returnFiber;
// 返回创建好的子Fiber
return created;
}
}
如果标签不同,那么react则会删除当前节点及兄弟节点,然后新建一个fiber节点
多节点
scss
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
mergeDebugInfo(debugInfo, newChild._debugInfo),
);
}
javascript
function reconcileChildrenArray(
returnFiber: Fiber, // 父节点
currentFirstChild: Fiber | null, // currentFiber
newChildren: Array<any>, // 本次更新的 jsx 对象数组
lanes: Lanes, // 优先级
debugInfo: ReactDebugInfo | null,
): Fiber | null {
/**
* 存储第一个子节点 Fiber 对象, 方法返回的也是第一个子节点 Fiber 对象
* 因为其他子节点 Fiber 对象都存储在上一个子 Fiber 节点对象的 sibling 属性中
*/
let resultingFirstChild: Fiber | null = null;
// 上一次创建的 Fiber 对象
let previousNewFiber: Fiber | null = null;
// 旧节点的 currentFiber(也就是当前遍历到的fiber)
let oldFiber = currentFirstChild;
// 新创建的fiber节点对应dom节点的索引位置,为了区分节点位置变化
let lastPlacedIndex = 0;
// 当前遍历 newChildren 的索引
let newIdx = 0;
// oldFiber 的下一个 oldFiber
let nextOldFiber = null;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// ...
}
if (newIdx === newChildren.length) {
// ...
}
if (oldFiber === null) {
// ...
}
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
// ...
}
if (shouldTrackSideEffects) {
// ...
}
if (getIsHydrating()) {
// ...
}
return resultingFirstChild;
}
多节点 diff 分为 3 中情况
- 节点更新
- 节点新增或减少
- 节点位置变化
由于 节点更新出现的概率大于节点新增或减少和节点位置变化,所以react会优先处理节点更新,如果不满足节点更新的情况下,才会去处理其他情况。
我们先熟悉下变量名
ini
/**
* 存储第一个子节点 Fiber 对象, 方法返回的也是第一个子节点 Fiber 对象
* 因为其他子节点 Fiber 对象都存储在上一个子 Fiber 节点对象的 sibling 属性中
*/
let resultingFirstChild: Fiber | null = null;
// 上一次创建的 Fiber 对象
let previousNewFiber: Fiber | null = null;
// 旧节点的 currentFiber(也就是当前遍历到的fiber)
let oldFiber = currentFirstChild;
// 新创建的fiber节点对应dom节点的索引位置,为了区分节点位置变化
let lastPlacedIndex = 0;
// 当前遍历 newChildren 的索引
let newIdx = 0;
// oldFiber 的下一个 oldFiber
let nextOldFiber = null;
首先,我们先看第一轮的遍历
js
let resultingFirstChild: Fiber | null = null;
// 上一次创建的 Fiber 对象
let previousNewFiber: Fiber | null = null;
// 旧节点的 currentFiber(也就是当前遍历到的fiber)
let oldFiber = currentFirstChild;
// 新创建的fiber节点对应dom节点的索引位置,为了区分节点位置变化
let lastPlacedIndex = 0;
// 当前遍历 newChildren 的索引
let newIdx = 0;
// oldFiber 的下一个 oldFiber
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,
debugInfo,
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
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;
oldFiber = nextOldFiber;
}
// ******
function updateSlot(
returnFiber: Fiber, // 父节点
oldFiber: Fiber | null, // currentFiber
newChild: any, // 需要更新的JSX对象
lanes: Lanes,
debugInfo: null | ReactDebugInfo,
): Fiber | null {
// Update the fiber if the keys match, otherwise return null.
const key = oldFiber !== null ? oldFiber.key : null;
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number' ||
(enableBigIntSupport && typeof newChild === 'bigint')
) {
if (key !== null) {
return null;
}
return updateTextNode(
returnFiber,
oldFiber,
'' + newChild,
lanes,
debugInfo,
);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
return updateElement(
returnFiber,
oldFiber,
newChild,
lanes,
mergeDebugInfo(debugInfo, newChild._debugInfo),
);
} else {
return null;
}
}
// ...
}
// ...
throwOnInvalidObjectType(returnFiber, newChild);
}
return null;
}
function updateElement(
returnFiber: Fiber,
current: Fiber | null,
element: ReactElement,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
// ...
}
if (current !== null) {
if (
current.elementType === elementType ||
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === current.type)
) {
// Move based on index
const existing = useFiber(current, element.props);
coerceRef(returnFiber, current, existing, element);
existing.return = returnFiber;
return existing;
}
}
// 如果标签不同,则无法复用currentFiber,需要基于 element 创建一个新的fiber
const created = createFiberFromElement(element, returnFiber.mode, lanes);
coerceRef(returnFiber, current, created, element);
created.return = returnFiber;
return created;
}
看了代码后,我们可以看到,如果 key 和 type 不同,则会跳出第一个循环。另外,如果 key 相同 type 不同导致不可复用,会将 oldFiber 标记 DELETION 的标记,并继续遍历
js
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
debugInfo,
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// 将 oldFiber 加入到 returnFiber 的 deletions 数组中,后续删除
deleteChild(returnFiber, oldFiber);
}
}
js
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// ...
}
// 当前遍历 newChildren 的索引 === newChildren的长度(最理想的结果)
if (newIdx === newChildren.length) {
// 删除其他还没有遍历的oldFiber
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 旧节点的 currentFiber 已经遍历完了,这种情况处于还有其他新增节点没遍历
// newChildren没遍历完,oldFiber遍历完(第二种情况)
if (oldFiber === null) {
// 需要遍历剩下的newChildren
for (; newIdx < newChildren.length; newIdx++) {
// 创建新的 fiber 节点
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
lanes,
debugInfo,
);
if (newFiber === null) {
continue;
}
// 标记为新增
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// newChildren 与 oldFiber 都没遍历完(第三种情况)
// oldFiber 生成 key 对应 fiber节点的映射
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 处理移动的节点
for (; newIdx < newChildren.length; newIdx++) {
// ...
}
if (shouldTrackSideEffects) {
// 删除 existingChildren 未匹配到的 fiber
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
当第一个遍历完成后,react 会判断当前是否将 newChildren遍历完成了,如果没有则会再去判断 oldFiber 是否有值,没有代表 newChildren 中都是新增节点。接下来遍历剩余的newChildren,通过newChildren[i].key就能在existingChildren中找到key相同的oldFiber。
js
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx], // 当前的 element
lanes,
debugInfo,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
// newFiber 是复用节点
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;
}
}
//*****************************
function updateFromMap(
existingChildren: Map<string | number, Fiber>,
returnFiber: Fiber,
newIdx: number,
newChild: any,
lanes: Lanes,
debugInfo: ReactDebugInfo | null,
): Fiber | null {
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number' ||
(enableBigIntSupport && typeof newChild === 'bigint')
) {
// 获取对应的 oldFiber
const matchedFiber = existingChildren.get(newIdx) || null;
return updateTextNode(
returnFiber,
matchedFiber,
'' + newChild,
lanes,
debugInfo,
);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
const matchedFiber =
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null; // 获取对应的 oldFiber
return updateElement(
returnFiber,
matchedFiber,
newChild,
lanes,
mergeDebugInfo(debugInfo, newChild._debugInfo),
);
}
// ...
}
// ...
throwOnInvalidObjectType(returnFiber, newChild);
}
return null;
}