我们知道react通过fiber的双缓存机制来实现Fiber树的更新,那么整个流程是怎样的?
构建与更新
简述
react会维护两棵fiber树,每次调度任务结束会进行commit。在mount或update的时候调用scheduleUpdateOnFiber,决定使用同步或者调度异步方法对每个节点去执行更新:调用beginWork构建一个新的fiber节点,并且打上flag(早版本叫effecttag),然后在complete收集一个effectList链表,最后执行commit替换,完成dom的增删改。
更新起始
mount时,reactDom.render(app,Element)中会执行render方法,创建一个fiberRoot当作根节点执行scheduleUpdateOnFiber方法。
update时,this.setState方法会利用事件优先级创建update对象变成链表挂在enqueueUpdate.shared.pending上,然后使用scheduleUpdateOnFiber方法
scheduleUpdateOnFiber
无论是mount还是update,scheduleUpdateOnFiber是fiber树构建的开始,他会根据当前的优先级lane使用同步的方法performSyncWorkOnRoot(开始时间为当前)去执行更新,或使用ensureRootIsScheduled去获取最紧急的lane去更新。
当然对于mount是直接调用performSyncWorkOnRoot,执行一次commit以后,在使用ensureRootIsScheduled去执行fiber树的构建。
ensureRootIsScheduled
这个方法会获取nextlanes,nextlanes是是更新时的任务优先级,每一次都会从nextlanes取最优先级位lane作为任务优先级,匹配对应优先级调度(scheduleCallback)调用performConcurrentWorkOnRoot 或者 直接执行performSyncWorkOnRoot,在其中会执行workloop workloop分两种,一种根据是否有剩余时间去执行,没有时间了则记录下当前的workinprogress后commit下次接着完成任务,一种是同步执行
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
beginWork
beginWork的作用主要是执行effect,对class组件完成state更新,完成fiber节点的创建,diff,复用,并对有effect的节点flag标记。简要后代码如下,会根据didReceiveUpdate决定调用bailoutOnAlreadyFinishedWork检查子树的更新逻辑决定完全复用还是接着往下遍历
如果是不复用的,根据不同的tag,最后会调用reconcileChildren方法比较fiber跟reactElement完成diff,返回子fiber节点继续进行beginWork,直到遍历到叶子节点进行completeWork。
function beginWork(current, workInProgress, renderLanes) {
// fiber为空创建
if(workInProgress._debugNeedsRemount && current !== null) return remountFiber(...)
// 根据props,context是否改变以及type判断是否可以沿用以前的fiber节点,lane不够的也移到后一次更新
// type 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
if (oldProps !== newProps || hasContextChanged() || (
workInProgress.type !== current.type )) {
didReceiveUpdate = true;
}
}else if (!includesSomeLane(renderLanes, updateLanes)) {
.....
}
....
switch(tag){
case FunctionComponent:
...
return updateFunctionComponent(current, workInProgress, _Component, resolvedProps, renderLanes);
case ClassComponent:
...
return updateClassComponent(current, workInProgress, _Component2, _resolvedProps, renderLanes);
...
}
diff逻辑
一般对于节点有增删改移四种操作
reconcileChildFibers会根据children的数量执行不同的函数
对于单节点
react diff 通过判断key跟type决定是否复用,具体的执行函数在reconcileSingleElement中,代码不贴了。
对于多节点
react会经过两轮遍历,第一轮寻找childrens[i]是否与Fiber key相同,如果不同打断遍历进入第二轮,key与tag都相同就会执行对应的更新操作,如果tag不同则会删除。
当childrens跟fiber都没遍历完, 对newChildren遍历剩下的节点,比较lastPlacedIndex与children[i]位置(lastPlacedIndex为之前复用的节点在oldfiber链中的最大位置),如果i大不动,小即后移并且更新lastPlacedIndex的位置。
举个例子
ABCDE => CABED ,第一轮遍历A不匹配B直接打断,lastPlacedIndex为0
遍历newChildren c在oldFiber为2,更新lastPlacedIndex为2,
A在oldFiber为0,对比lastPlacedIndex进行移动,B同理,E不动且更新lastPlacedIndex位置,D移动
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
/* * returnFiber:currentFirstChild的父级fiber节点
* currentFirstChild:当前执行更新任务的WIP(fiber)节点
* newChildren:组件的render方法渲染出的新的ReactElement节点
* lanes:优先级相关
* */
// resultingFirstChild是diff之后的新fiber链表的第一个fiber。
let resultingFirstChild: Fiber | null = null;
// resultingFirstChild是新链表的第一个fiber。
// previousNewFiber用来将后续的新fiber接到第一个fiber之后
let previousNewFiber: Fiber | null = null;
// oldFiber节点,新的child节点会和它进行比较
let oldFiber = currentFirstChild;
// 存储固定节点的位置
let lastPlacedIndex = 0;
// 存储遍历到的新节点的索引
let newIdx = 0;
// 记录目前遍历到的oldFiber的下一个节点
let nextOldFiber = null;
// 该轮遍历来处理节点更新,依据节点是否可复用来决定是否中断遍历
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// newChildren遍历完了,oldFiber链没有遍历完,此时需要中断遍历
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber; oldFiber = null;
} else {
// 用nextOldFiber存储当前遍历到的oldFiber的下一个节点
nextOldFiber = oldFiber.sibling;
}
// 生成新的节点,判断key与tag是否相同就在updateSlot中
// 对DOM类型的元素来说,key 和 tag都相同才会复用oldFiber
// 并返回出去,否则返回null
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
// newFiber为 null说明 key 或 tag 不同,节点不可复用,中断遍历
if (newFiber === null) {
if (oldFiber === null) {
// oldFiber 为null说明oldFiber此时也遍历完了
// 是以下场景,D为新增节点
// 旧 A - B - C
// 新 A - B - C - D oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
// shouldTrackSideEffects 为true表示是更新过程
if (oldFiber && newFiber.alternate === null) {
// newFiber.alternate 等同于 oldFiber.alternate
// oldFiber为WIP节点,它的alternate 就是 current节点
// oldFiber存在,并且经过更新后的新fiber节点它还没有current节点,
// 说明更新后展现在屏幕上不会有current节点,而更新后WIP
// 节点会称为current节点,所以需要删除已有的WIP节点
deleteChild(returnFiber, oldFiber);
}
}
// 记录固定节点的位置
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 将新fiber连接成以sibling为指针的单向链表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
// 将oldFiber节点指向下一个,与newChildren的遍历同步移动
oldFiber = nextOldFiber;
}
// 处理节点删除。新子节点遍历完,说明剩下的oldFiber都是没用的了,可以删除.
if (newIdx === newChildren.length) {
// newChildren遍历结束,删除掉oldFiber链中的剩下的节点
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 处理新增节点。旧的遍历完了,能复用的都复用了,所以意味着新的都是新插入的了
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
// 基于新生成的ReactElement创建新的Fiber节点
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
// 记录固定节点的位置lastPlacedIndex
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 将新生成的fiber节点连接成以sibling为指针的单向链表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// 执行到这是都没遍历完的情况,把剩余的旧子节点放入一个以key为键,值为oldFiber节点的map中
// 这样在基于oldFiber节点新建新的fiber节点时,可以通过key快速地找出oldFiber
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 节点移动
for (; newIdx < newChildren.length; newIdx++) {
// 基于map中的oldFiber节点来创建新fiber
const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes, );
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// 因为newChildren中剩余的节点有可能和oldFiber节点一样,只是位置换了,
// 但也有可能是是新增的.
// 如果newFiber的alternate不为空,则说明newFiber不是新增的。
// 也就说明着它是基于map中的oldFiber节点新建的,意味着oldFiber已经被使用了,所以需
// 要从map中删去oldFiber
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
// 移动节点,多节点diff的核心,这里真正会实现节点的移动
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 将新fiber连接成以sibling为指针的单向链表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// 此时newChildren遍历完了,该移动的都移动了,那么删除剩下的oldFiber
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
completeWork
completeWork主要
- 收集effectlist
- 对hostComponent操作dom
- dom的属性处理
- 抛出异常节点
未完待续。。。