我们都知道react分为reconcile和render两个过程,reconcile过程负责找出变化的虚拟dom,render过程负责去更新这些变化的虚拟dom。那如何去以一个尽可能小的代价去找出变化的虚拟dom呢?
诞生背景
由于Diff操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的时间复杂程度也要 O(n^3),所以react为了降低算法复杂度,他做了以下几点。
- react只对同级元素进行Diff。但是同级节点通过增加,删除,替换操作来实现复用,最少需要
O(n^2)。react团队觉得时间复杂度还是太高了。 - 所以通过标记 key 来降低复杂度,做到
O(n)。如果key和type都相同,即可以复用。例如,如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。
思路
todo...
todo...
代码如何实现
这是我自己实现的,难免有bug,有问题在评论区留言。。
这里要注意,oldFiber是一条以sibling属性连起来的链表。newChildren是数组,是从fiber节点上拿下来的(fiber.props.children)。
要用到的功能函数
判断能否复用
export function isSameType(a, b) {
return a && b && a.type === b.type && a.key === b.key;
}
删除节点
function deleteChild(returnFiber, childToDelete) {
let deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete];
// returnFiber.flags |= ChildDeletion;
} else {
deletions.push(childToDelete)
}
}
function deleteRemainingChildren(returnFiber, currentFirstChild) {
let childToDelete = currentFirstChild;
while (childToDelete) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
}
把老fiber节点放入map中,方便后续查找
function mapRemainingChildren(oldFiber) {
const existingChildren = new Map();
let prev = oldFiber;
while (prev) {
existingChildren.set(prev.key || prev.index, prev);
prev = prev.sibling;
}
return existingChildren;
}
更新lastPlacedIndex并为fiber节点打上flags
function placeIndex(newFiber, lastPlacedIndex, index, shouldTrackSideEffects) {
newFiber.index = index;
// 父节点 是否初次渲染
if (!shouldTrackSideEffects) {
return lastPlacedIndex
} else {
// 子节点 是否初次渲染
const current = newFiber.alternate;
if (current) {
const oldIndex = current.index;
if (oldIndex >= lastPlacedIndex) {
return oldIndex;
} else {
newFiber.flags |= Placement;
return lastPlacedIndex;
}
} else {
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}
}
diff实现
首先,需要进行初始化
export function reconcileChildren(returnFiber, children) {
let lastPlacedIndex = 0;// 上次插入的位置
let shouldTrackSideEffects = !!returnFiber.alternate; //是否初次渲染
let nextOldFiber; //暂存oldFiber
if (isStringOrNumber(children)) {
return;//文本节点直接返回
}
// todo 单子节点和多子节点分开处理,这里都当做多子节点处理
const newChildren = isArray(children) ? children : [children]
let previousFiber = null;
let oldFiber = returnFiber.alternate?.child;
// .........................
}
1.如果有oldFiber,就将oldFiber和newChildren挨个对比,符合条件就复用,不符合就退出循环。此时,有4种可能结果(2,3,4,5)
let i = 0;
for (; oldFiber && i < newChildren.length; i++) {
const newChild = newChildren[i];
if (newChild === null || newChild === undefined) {
continue;
}
if (oldFiber.index !== i) {
nextOldFiber = oldFiber;
oldFiber = null;// 会跳出循环
} else {
nextOldFiber = oldFiber.sibling;
}
// type和key都相同才返回true
if (!isSameType(oldFiber, newChild)) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
const newFiber = createFiber(newChild, returnFiber);
Object.assign(newFiber, {
flags: Update,
stateNode: oldFiber.stateNode,
alternate: oldFiber
})
console.log('复用成功1', oldFiber, newFiber)
lastPlacedIndex = placeIndex(newFiber, lastPlacedIndex, i, shouldTrackSideEffects)
if (previousFiber === null) {
returnFiber.child = newFiber;
} else {
previousFiber.sibling = newFiber;
}
previousFiber = newFiber;
oldFiber = nextOldFiber;
}
2.oldFiber没遍历完,newChildren遍历完。
- 意味着本次更新比之前的节点数量少,有节点被删除了。所以需要遍历剩下的oldFiber,依次放入父fiber节点的deletions数组,后续在commit阶段(也就是前面所说的render阶段)再去删除。
if (i === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return;
}
3.newChildren没遍历完,oldFiber遍历完。
(初次渲染也属于这种情况,因为没有oldFiber)
已有的DOM节点都复用了,这时还有新加入的节点,意味着本次更新有新节点插入,我们只需要遍历剩下的newChildren为生成的workInProgress fiber依次标记Placement。
if (!oldFiber) {
for (; i < newChildren.length; i++) {
const newChild = newChildren[i];
// 比如写jsx时写成这样 {num?<span>111</span>:null} ,null不渲染
if (newChild === null || newChild === undefined) {
continue;
}
const newFiber = createFiber(newChild, returnFiber);
lastPlacedIndex = placeIndex(newFiber, lastPlacedIndex, i, shouldTrackSideEffects);
if (previousFiber === null) {
returnFiber.child = newFiber;
} else {
previousFiber.sibling = newFiber;
}
previousFiber = newFiber;
}
}
4.newChildren和oldFiber都没遍历完,比较麻烦。
const existingChildren = mapRemainingChildren(oldFiber);
for (; i < newChildren.length; i++) {
const newChild = newChildren[i];
if (newChild === null || newChild === undefined) {
continue;
}
const shouldAlternate = existingChildren.has(newChild.key || newChild.index);
const newFiber = createFiber(newChild, returnFiber);
if (shouldAlternate) {
const oldFiber = existingChildren.get(newChild.key || newChild.index);
Object.assign(newFiber, {
flags: Update,
stateNode: oldFiber.stateNode,
alternate: oldFiber
})
lastPlacedIndex = placeIndex(newFiber, lastPlacedIndex, i, shouldTrackSideEffects)
console.log('复用成功4', oldFiber, newFiber)
existingChildren.delete(newChild.key || newChild.index);
}
if (previousFiber === null) {
returnFiber.child = newFiber;
} else {
previousFiber.sibling = newFiber;
}
previousFiber = newFiber;
}
// 还要删除existingChildren剩下的
// 更新阶段才可能有existingChildren,所有外层加个if
if (shouldTrackSideEffects) {
for (const [, fiber] of existingChildren) {
console.log('删除existingChildren剩下的')
deleteChild(returnFiber, fiber);
}
}