React 17 Fiber树的构建与更新(上)

1,393 阅读5分钟

我们知道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主要

  1. 收集effectlist
  2. 对hostComponent操作dom
  3. dom的属性处理
  4. 抛出异常节点

未完待续。。。