diff 比较
diff 比较只会同级比较,如果父节点标记为新建/删除,子节点也不会进行比较;
如果代码看的比较烦,可以直接跳到底部图形描述
单节点
判断单节点是更新之后的节点计算,即使更新之前的多节点,更新之后是单节点。
遍历更新之前的节点,如果key和type都相同,这个节点会复用,其他节点将标记为删除状态,如果没有符合的话,就将之前所有的节点都标记成删除状态。
如果更新后是文本,更新前第一个节点是文本节点,更新内容,将其他fiber标记为删除,否则都删除,创建一个新文本节点
多节点
多节点是diff算法中的重点
- 首先第一次遍历
js
let resultingFirstChild = null;
let previousNewFiber = null;
// current树中的第一个子节点
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// 遍历更新后的fiber节点
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,
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
// 更新阶段 shouldTrackSideEffects = true
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
首先遍历更新后的fiber节点,调用updateSlot方法进行处理,
js
const key = oldFiber !== null ? oldFiber.key : null;
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
if (key !== null) {
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) {
return updateElement(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
}
}
case REACT_PORTAL_TYPE: {
if (newChild.key === key) {
return updatePortal(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
}
}
case REACT_LAZY_TYPE: {
const payload = newChild._payload;
const init = newChild._init;
return updateSlot(returnFiber, oldFiber, init(payload), lanes);
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
if (key !== null) {
return null;
}
return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
return null;
如果当前节点是文本/数字,current中当前节点有存在key,返回null, newFiber == null; break; 那么当前遍历结束;如果不存在key,调用updateTextNode
方法,
js
if (current === null || current.tag !== HostText) {
// Insert
const created = createFiberFromText(textContent, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
} else {
// Update
const existing = useFiber(current, textContent);
existing.return = returnFiber;
return existing;
}
如果不是文本节点就创建一个文本节点返回,如果存在oldFiber节点,将这个节点标记删除deleteChild(returnFiber, oldFiber); ,否则就用当前的节点更新一个新的节点;
如果是非文本节点,新老节点的key进行比较,如果不同就退出当前循环;如果相同调用updateElement
方法,如果是elementType
相同,更新当前的fiber,否则创建一个新的fiber节点;
第一次遍历为了找到从开始符合的节点,当第一个节点不符合的时候就结束;
- 判断新节点是否完成
js
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
// hydrate 情况下, 默认渲染都是false
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
deleteRemainingChildren方法
js
let childToDelete = currentFirstChild;
while (childToDelete !== null) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
return null;
如果新节点遍历完成,将旧节点剩余的都标记为删除态,并且返回新节点。
- 如果新节点未完成,旧节点没有完成,
js
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;
}
// 忽略这个条件
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
遍历剩余当前节点,调用createChild 方法,创建新的fiber节点。
- 调用
mapRemainingChildren
方法
js
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;
遍历旧fiber节点,将fiber节点存到Map对象对象中,Map中以key || index 为键,fiber为值。
- 继续遍历剩余新节点
js
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
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;
}
}
遍历剩余新节点,调用updateFromMap
方法,
js
if (
(typeof newChild === 'string' && newChild !== '') ||
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;
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:
const payload = newChild._payload;
const init = newChild._init;
return updateFromMap(
existingChildren,
returnFiber,
newIdx,
init(payload),
lanes,
);
}
if (isArray(newChild) || getIteratorFn(newChild)) {
const matchedFiber = existingChildren.get(newIdx) || null;
return updateFragment(returnFiber, matchedFiber, newChild, lanes, null);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
return null;
如果是字符/数字,通过index作为key查找Map的值,如果旧fiber也是一个文本就更新,否则创建一个文本节点。
如果是dom节点,以key || index 为键,查找Map中fiber,如果新老elementType
相同,以旧节点更新,否则创建一个新节点。
如果新节点不为null,为更新节点,将Map中的旧节点删除,
处理lastPlacedIndex , 如果是新建节点,lastPlacedIndex 不变,否则找到current树下的index,返回lastPlacedIndex 和index 中的最大值,并将lastPlacedIndex赋值;如果lastPlacedIndex > oldIndex
,当前节点为移动,
遍历Map,将旧节点标记为删除。
图形对比
相同的形状代表相同的elementType,a,b,c... 代表不同的key;
- a,b,c 作为第一次遍历,只是更新属性,不会对节点移动删除等处理,d节点新增
2. a,b,c 作为第一次遍历,只是更新属性,不会对节点移动删除等处理,d节点删除
3. 删除旧节点,新建节点
4. 将a,b节点移动,c,d节点不动
5. d节点不动,a,b,c节点移动
- 第一次遍历,a节点不变,第二次遍历b,d移动,e删除,g新增;
- 第一次遍历,a,b遍历,但是新a节点新建,旧a节点删除,第二次遍历,创建e,h节点,删除d,e节点,f移动。
8. 第一次遍历,第一个节点新建,b不变,第二次遍历,e,h和最后一个节点新建,d,f,g节点删除,c和第五个节点不变。 (第一次遍历如果当前节点和对应index的旧节点都不存在,就认为也是相同的key,第二次遍历,当前节点不存在key,那么Map中的键是index,遍历的时候会找到旧节点对应的index的fiber)
9. 第一次遍历,第一个节点和b,第二次遍历,c节点移动,h和最后一个节点新建,d和第五个节点删除。
10. 第一次遍历,a,b节点不变,第二次遍历,第二个a节点由old节点中第二个a节点移动,第三个a新建,e节点新建,d,f节点删除。(控制台会报错,key重复)
11. 第一次遍历,a,b节点不变,第二次遍历,因为Map存储,键是key,如果重复,后面的节点把前面的节点替换掉,所以最后两个a是新建,第三个a和c移动,第二个a,b删除。
总结
react会双遍历,如果第一次遍历发现新节点和旧节点的key顺序相同(前后没有key也算相同),就不会会第二次遍历,第一次遍历之后发现没有新节点,旧节点都标记为删除,没有旧节点就将新节点标记为新增;如果上述情况不符,那么会遍历剩余的新节点,react采取的是右移策略(针对相同type和key),找到当前节点前一个非新增的节点,如果不存在,那么当前节点不变,遍历下一个节点;存在的情况下,找到old节点下的对应节点,如果当前节点在上一个节点的左侧,那么就右移,否则不变。