diff算法,也是调和过程,就是[[react]]用新的虚拟dom去更新现有的fiber节点的过程。
// 入口函数,主要任务是根据新的虚拟dom的个数来调用不同diff算法
// 单个: reconcileSingleElement
// 多个:reconcileChildrenArray
// 文本或数字:reconcileSingleTextNode
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber: null,
newChild: any
) {
if (typeof newChild === 'object' && newChild !== null){
// 新的子节点是【单个】虚拟dom
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild
)
);
}
// 新的子节点是【数组】
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild
)
}
}
// 新子元素是文本或数字
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild
)
)
}
// 说明旧的fiber节点有多余,删除
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
diff单个元素
// 单个虚拟dom
// 尝试复用key和elementType都一样的旧fiber节点
// 复用不了就直接创建新的
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstFiber: Fiber | null,
element: ReactElement
) {
const key = element.key;
let child = currentFirstChild;
while(child !== null) {
if (child.key === key) {
// fiber.elementType保存是组件的类型
// 函数组件就是函数,类组件就是类定义,浏览器标签就是标签名
if (element.type === child.elementType) {
// 组件类型一致,则复用旧的fiber节点
deleteReainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.return = returnFiber;
return existing;
}
// 类型不同,则直接删除当前以及后面的所有兄弟节点
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 走到这里说明没能复用,则新建一个fiber节点
const created = createFiberFromElement(element, returnFiber.mode);
created.return = returnFiber;
return created;
}
小结
- 尝试复用相同key的旧fiber元素
- 如果key一致,但是组件类型不一致,也不会继续查找兄弟节点
diff一个数组
// diff一个数组
// 因为fiber节点是单向链表,所以不能使用【两端匹配】
// 只能的从前往后进行复用尽量多的一样的key,一旦遇到key不匹配则直接使用map匹配
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: Array<*>
): Fiber | null {
// 第一次循环
// 是一个老的fiber节点和一个新的虚拟dom 严格从前往后一对一复用相同key
// 结束条件:老的fiber节点没有兄弟节点了,新的虚拟dom到最后一个了,一对一复用时key不一致
let oldFiber = currentFirstChild;
let newIdx = 0;
let lastPlacedIndex = 0;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 当key一致时,用newChildren[newIdx]去更新oldFiber,得到newFiber
// 否则updateSlot返回null
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx]
)
if (newFiber === null) {
// 只能使用map匹配了,见第四次循环。
break;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// 保存第一个fiber节点,因为需要返回
resultingFirstChild = newFiber;
} else {
// 通过sibling串联起来
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = oldFiber.sibling;
}
// ...暂时忽略后面的代码
}
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number
) {
// newIndex就是newFiber的正确位置
newFiber.index = newIndex;
const current = newFiber.alternate;
if (current !== null) {
// 说明是更新fiber节点的位置
// oldIndex在屏幕上渲染的fiber节点的位置
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// oldIndex需要移动位置
return lastPlacedIndex;
} else {
// oldIndex === lastPlacedIndex,说明老fiber节点位置没有发生变化
// oldIndex > lastPlacedIndex,说明老fiber节点向前移动
return oldIndex;
}
} else {
// 说明newFiber是新增
return lastPlacedIndex;
}
}
移动举例:
old: a -> b -> c -> d
new: [a, d, b, c]
a: 因为oldIndex === lastPlacedIndex, 所以返回oldIndex 0
d: 因为oldIndex > lastPlacedIndex,oldPlacedIndex更新成oldIndex 3
b: 因为oldIndex < lastPlacedIndex,oldPlacedIndex不变,但b需要移动
c: 因为oldIndex > lastPlacedIndex,oldPlacedIndex不变,但c需要移动
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstFiber: Fiber | null,
element: ReactElement
) {
// 第一次循环
// ...代码省略
// 第二次循环
// 判断第一次循环结束是不是因为新的虚拟dom到最后了
// 目的:删除旧的fiber节点即可
if (newIdx === newChildren.length) {
// 说明第一次循环,那么只需要删除多余的旧的节点
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 第三次循环
// 判断第一次循环结束的原因老的fiber节点到最后
// 目的:为多余的新的虚拟dom创建对应的fiber节点
if (oldFiber === null) {
// 说明旧的节点已经结束,那么就为多余的新节点创建对应的fiber
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);
if (previousNewFiber === null) {
// 保存第一个fiber节点,因为需要返回
resultingFirstChild = newFiber;
} else {
// 通过sibling串联起来
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// 第四次循环
// 就是第一次循环中途遇到key不一样的情况
// 将余下的fiber的key或者index当作map,用于复用
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFormMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx]
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// 将旧fiber从existingChildren中删除,避免最后被删除。
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key
)
}
}
lastPlacedFirstChild = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// 余下的fiber都是需要删除的
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
小结
- 会严格尝试复用旧fiber链的前n个元素,直到第n+1个fiber对象和第n+1个虚拟dom key不一致,则不再继续复用。
- 如果全部的虚拟dom都被复用了,只需要删除多余的旧fiber对象
- 如果全部的fiber对象都被复用了,只需要为多余的虚拟dom创建对应的fiber对象
- 那么只能通过key来复用老的fiber对象了
shouldTrackSideEffects
之前的代码段中多处使用了shouldTrackSideEffects,大多数都是作为设置flags之前的判断条件,本质是react区别对待了初次挂载阶段和更新阶段:如果一个fiber节点是初次挂载,那么其后代fiber节点都不需要设置flags,只需要设置初次挂载的fiber节点即可;但是更新阶段,其后代fiber节点需要在diff过程中才知道将要更新、删除或添加,所以在diff过程中设置flags。
// 这里是工厂模式,入参就是shouldTrackSideEffects
function ChildReconciler(shouldTrackSideEffects) {
function deleteChild() {...}
function placeChild() {...}
function placeSingleChild() {...}
function createChild() {...}
function updateFromMap() {...}
function reconcileChildrenArray() {...}
function reconcileSingleTextNode() {...}
function reconcileSingleElement() {...}
function reconcileChildFibers() {...}
// 就是第一个函数,真正实现功能的函数。
return reconcileChildFibers;
}
// 使用工厂函数创建2个真正实现功能的函数
const reconcileChildFibers = ChildReconciler(true);
const mountChildFibers = ChildReconciler(false);
function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any
) {
if (current === null) {
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren)
} elese {
workInProgress.child = reconcileChildFibers(workInProgress, current, nextChildren)
}
}
小结
- 挂载节点和更新节点的调和算法的代码是一套,仅仅用
shouldTrackSideEffects来是否设置flags