• react17.x源码解析(2)——fiber树的构建与更新


    下面我们结合源码,来看一下实际工作过程中fiber树的构建与更新过程。

    mount过程
    react首次mount开始执行时,以ReactDOM.render为入口函数,会经过如下一系列的函数调用:
    ReactDOM.render——> legacyRenderSubtreeIntoContainer——>legacyCreateRootFromDOMContainer——>createLegacyRoot——>ReactDOMBlockingRoot——>ReactDOMRoot——>createRootImpl——>createContainer——>createFiberRoot——>createHostFiber——>createFiber

    在createFiber函数中,调用FiberNode构造函数,创建了rootFiber,他是react应用的根fiber:

    // packages/react-reconciler/src/ReactFiber.old.js
    
    const createFiber = function(
      tag: WorkTag,
      pendingProps: mixed,
      key: null | string,
      mode: TypeOfMode,
    ): Fiber {
      return new FiberNode(tag, pendingProps, key, mode);
    };
    

    在createFiberRoot函数中,调用FiberRootNode构造函数,创建了fiberRoot,他指向真实根dom节点。

    // packages/react-reconciler/src/ReactFiberRoot.old.js
    
    export function createFiberRoot(
      containerInfo: any,
      tag: RootTag,
      hydrate: boolean,
      hydrationCallbacks: null | SuspenseHydrationCallbacks,
    ): FiberRoot {
      const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
      if (enableSuspenseCallback) {
        root.hydrationCallbacks = hydrationCallbacks;
      }
    
      const uninitializedFiber = createHostRootFiber(tag);
      root.current = uninitializedFiber;
      uninitializedFiber.stateNode = root;
    
      initializeUpdateQueue(uninitializedFiber);
    
      return root;
    }
    

    另外 createFiberRoot函数中,还让rootFiber的stateNode字段指向了fiberRoot,fiberRoot的current字段指向了rootFiber。从而一颗最原始的fiber树根节点就创建完成了:
    image
    上面的rootFiber和fiberRoot创建完成后,react就会根据jsx的内容去创建详细的dom树了,例如有如下的jsx:
    image

    react对于fiber结构的创建和 更新,都是采用深度优先遍历,从rootFiber(此处对应id为root的节点)开始,首先创建child a1,然后发现a1有子节点b1,继续对b1进行遍历,b1有子节点c1,再去创建c1的子节点d1、d2、d3,直至发现d1、d2、d3都没有子节点来了,再回去创建c2.

    上面的过程中,每个节点开始创建时,执行beginWork流程,直至该节点的所有子孙节点都创建(更新)完成后,执行completeWork流程,流程的图示如下:
    image

    update过程
    update时,react会根据新的jsx内容创建新的workInProgress fiber,还是通过深度优先遍历,对发生改变的fiber打上不同的flags副作用标签,并通过firstEffect、nextEffect等字段形成Effect List链表。
    例如上面的jsx结构,发生了如下的更新:

    image
    react会根据新的jsx解析后的内容调用createWorkInProgress函数创建workInProgress fiber,对其标记副作用:

    // packages/react-reconciler/src/ReactFiber.old.js
    
    export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
      let workInProgress = current.alternate;
      if (workInProgress === null) { // 区分 mount 还是 update
        workInProgress = createFiber(
          current.tag,
          pendingProps,
          current.key,
          current.mode,
        );
        workInProgress.elementType = current.elementType;
        workInProgress.type = current.type;
        workInProgress.stateNode = current.stateNode;
    
        if (__DEV__) {
          workInProgress._debugID = current._debugID;
          workInProgress._debugSource = current._debugSource;
          workInProgress._debugOwner = current._debugOwner;
          workInProgress._debugHookTypes = current._debugHookTypes;
        }
    
        workInProgress.alternate = current;
        current.alternate = workInProgress;
      } else {
        workInProgress.pendingProps = pendingProps;
        workInProgress.type = current.type;
    
        workInProgress.subtreeFlags = NoFlags;
        workInProgress.deletions = null;
    
        if (enableProfilerTimer) {
          workInProgress.actualDuration = 0;
          workInProgress.actualStartTime = -1;
        }
      }
    
      // 重置所有的副作用
      workInProgress.flags = current.flags & StaticMask;
      workInProgress.childLanes = current.childLanes;
      workInProgress.lanes = current.lanes;
    
      workInProgress.child = current.child;
      workInProgress.memoizedProps = current.memoizedProps;
      workInProgress.memoizedState = current.memoizedState;
      workInProgress.updateQueue = current.updateQueue;
    
      // 克隆依赖
      const currentDependencies = current.dependencies;
      workInProgress.dependencies =
        currentDependencies === null
          ? null
          : {
              lanes: currentDependencies.lanes,
              firstContext: currentDependencies.firstContext,
            };
    
      workInProgress.sibling = current.sibling;
      workInProgress.index = current.index;
      workInProgress.ref = current.ref;
    
      if (enableProfilerTimer) {
        workInProgress.selfBaseDuration = current.selfBaseDuration;
        workInProgress.treeBaseDuration = current.treeBaseDuration;
      }
    
      if (__DEV__) {
        workInProgress._debugNeedsRemount = current._debugNeedsRemount;
        switch (workInProgress.tag) {
          case IndeterminateComponent:
          case FunctionComponent:
          case SimpleMemoComponent:
            workInProgress.type = resolveFunctionForHotReloading(current.type);
            break;
          case ClassComponent:
            workInProgress.type = resolveClassForHotReloading(current.type);
            break;
          case ForwardRef:
            workInProgress.type = resolveForwardRefForHotReloading(current.type);
            break;
          default:
            break;
        }
      }
    
      return workInProgress;
    }
    

    最终生成的 workInProgress fiber图示如下:
    image
    然后如上面所说,current fiber和workInProgress fiber中对应的alternate会相互指向,然后workInProgress fiber完全创建完成后,fiberRoot的current字段的指向会从current fiber中的rootFiber改为workInProgress fiber 中的rootFiber:
    image

    然后如上面所说,current fiber和workInProgress fiber中对应的alternate会相互指向,然后workInProgress fiber 完全创建完成后,fiberRoot的current字段的指向会从current fiber中的rootFiber改为workInProgress fiber中的rootFiber:
    image

    下面我们将探究以下部分内容的源码:

    • 更新任务的触发
    • 更新任务的插件
    • reconciler过程同步和异步遍历及执行任务
    • scheduler是如何实现帧空闲时间调度任务以及中断任务的

    触发更新

    触发更新的方式主要有以下几种:ReactDOM.render、setState、forUpdate以及hooks中的useState等,关于hooks的我们后面再详细讲解,这里先关注前三种情况。

    ReactDOM.render

    ReactDOM.render作为react应用程序的入口函数,在页面首次渲染时便会触发,页面dom的首次创建,也属于触发react更新的一种情况。其整体流程如下:
    image

    首先调用legacyRenderSubtreeIntoContainer函数,校验根节点root是否存在,若不存在,调用legacyCreateRootFromDOMContainer创建根节点root、rootFiber和fiberRoot并绑定他们之间的引用关系,然后调用updateContaioner去批量执行后面的更新流程;若存在,直接调用updateContainer去批量执行后面的更新流程:

    // packages/react-dom/src/client/ReactDOMLegacy.js
    
    function legacyRenderSubtreeIntoContainer(
      parentComponent: ?React$Component<any, any>,
      children: ReactNodeList,
      container: Container,
      forceHydrate: boolean,
      callback: ?Function,
    ) {
      // ...
      let root: RootType = (container._reactRootContainer: any);
      let fiberRoot;
      if (!root) {
        // 首次渲染时根节点不存在
        // 通过 legacyCreateRootFromDOMContainer 创建根节点、fiberRoot 和 rootFiber
        root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
          container,
          forceHydrate,
        );
        fiberRoot = root._internalRoot;
        if (typeof callback === 'function') {
          const originalCallback = callback;
          callback = function() {
            const instance = getPublicRootInstance(fiberRoot);
            originalCallback.call(instance);
          };
        }
        // 非批量执行更新流程
        unbatchedUpdates(() => {
          updateContainer(children, fiberRoot, parentComponent, callback);
        });
      } else {
        fiberRoot = root._internalRoot;
        if (typeof callback === 'function') {
          const originalCallback = callback;
          callback = function() {
            const instance = getPublicRootInstance(fiberRoot);
            originalCallback.call(instance);
          };
        }
        // 批量执行更新流程
        updateContainer(children, fiberRoot, parentComponent, callback);
      }
      return getPublicRootInstance(fiberRoot);
    }
    

    updateContainer函数中,主要做了以下几件事情:

    • requestEventTime:获取更新触发的时间
    • requestUpdateLane:获取当前任务优先级
    • createUpdate:创建更新
    • enqueueUpdate:将任务推进更新队列
    • scheduleUpdateOnFiber:调度更新

    关于这几个函数稍后会详细降到:

    export function updateContainer(
    element:ReactNodeList,
    container:OpaqueRoot,
    parentComponent:?React$Component<any,any>,
    callback:?Function,
    ):Lane{
    // ...
    const current = container.current;
    const eventTime = requestEventTime();//获取更新触发的时间
    // ...
    const lane = requestUpdateLane(current);//获取任务优先级
    if (enableSchedulingProfiler){
    markRenderScheduled(lane);
    }
    const context = getContextForSubtree(parentComponent);
    if (container.context === null){
     container.context = context;
    }else{
    container.pendingContext = context;
    }
    const update = createUpdate(eventTime,lane);//创建更新任务
    update.payload = {element};
    callback = callback === undefined ? null : callback;
    if(callback !== null){
     update.callback = callback;
    }
     enqueueUpdate(current,update);//将任务推入更新队列
     scheduleUpdateOnFiber(current,lane,eventTime);//schedule进度调度
     return lane;
    }
    

    setState

    setState是类组件中我们最常用的修改状态的方法,状态修改会触发更新流程,其执行过程如下:
    image

    class组件在原型链上定义了setState方法,其调用了触发器updater上的enqueueSetState方法:
    image
    然后我们再来看以下updater上定义的enqueueSetState方法,一看到这我们就了然了,和updateContainer方法中做的事情几乎一模一样,都是触发后续的更新调度。
    image

    forceUpdate
    forceUpdate的流程与setState几乎一模一样:
    image
    同样其调用了触发器updater上的enqueueForceUpdate方法,enqueueForceUpdate方法也同样是触发了一系列的更新流程:

    image

    创建更新任务

    可以发现,上述的三种触发更新的动作,最后殊途同归,都会走上上述流程图中从requestEventTime到scheduleUpdateOnFiber这一流程,去创建更新任务,我们先详细看一下更新任务是如何创建的。

    获取更新触发时间

    前面讲到过,react执行更新过程中,会将更新任务拆解,每一帧优先执行高优先级的任务,从而保证用户体验的流畅。那么即使对于同样优先级的任务,在任务多的情况下该优先执行哪一些呢?

    react通过requestEventTime方法去创建一个currentEventTime,用于标识更新任务触发的时间,对于相同时间的任务,会批量去执行。同样优先级的任务,currentEventTime值越小,就会越早执行。

    我们看一下requestEventTime方法的实现:

    image

    在这个方法中,(executionContext & (RenderContext | CommitContext))做了二进制运算,RenderContext代表着react正在计算更新,CommitContext代表着react正在提交更新。所以这句话是判断当前react是否处在计算或者提交更新的阶段,如果是则直接返回now()。
    image
    再来看一下now的代码,这里的意思是,当前后的更新任务时间差小于10ms时,直接采用上次的Scheduler_now,这样可以抹平10ms内更新任务的时间差,有利于批量更新:

    // packages/react-reconciler/src/SchedulerWithReactIntegration.old.js
    export const now = initailTimeMs < 10000 ? Scheuler_now : ()=>Scheduler_now() - initialTimeMs;
    

    综上所述,requestEvent做的事情如下:

    • 在react的render和commit阶段我们直接获取更新任务的触发事件,并抹平相差10ms以内的更新任务以便批量执行。
    • 当currentEventTime不等于NoTimestamp时,则判断其正在执行浏览器事件,react想要同样优先级的更新任务保持相同的时间,所以直接返回上次的currentEventTime
    • 如果是react上次中断之后的首次更新,那么给currentEventTime赋一个新的值

    划分更新任务优先级
    说完了相同优先级任务的触发时间,那么任务的优先级又是如何划分的呢?这里就要提到requestUpdateLane,我们来看一下源码:
    image
    它首先找出会通过getCurrentPriorityLevel方法,根据Scheduler中记录的事件优先级,获取任务调度的优先级schedulerPriority。然后通过findUpdateLane方法计算得出lane,作为更新过程中的优先级。

    findUpdateLane这个方法中,按照事件的类型,匹配不同级别的lane,事件类型的优先级划分如下,值越高,代表的优先级越高:
    image

    创建更新对象
    eventTime和lane都创建好了之后,就该创建更新了,createUpdate就是基于上面两个方法所创建的eventTime和lane,去创建一个更新对象:
    image

    关联fiber的更新对列

    创建好了update对象之后,紧接着调用enqueueUpdate方法把update对象放到关联的fiber的updateQueue对列之中:

    // packages/react-reconciler/src/ReactUpdateQueue.old.js
    
    export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
      // 获取当前 fiber 的更新队列
      const updateQueue = fiber.updateQueue;
      if (updateQueue === null) {
        // 若 updateQueue 为空,表示 fiber 还未渲染,直接退出
        return;
      }
    
      const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
      const pending = sharedQueue.pending;
      if (pending === null) {
        // pending 为 null 时表示首次更新,创建循环列表
        update.next = update;
      } else {
        // 将 update 插入到循环列表中
        update.next = pending.next;
        pending.next = update;
      }
      sharedQueue.pending = update;
    
      // ...
    }
    

    根据任务类型执行不同更新
    reconciler阶段会协调任务去执行,以scheduleUpdateOnFiber为入口函数,首先会调用checkForNestedUpdates方法,检查嵌套的更新数量,若嵌套数量大于50层时,被认为是循环更新(无限更新)。此时会抛出异常,避免了例如在类组件render函数中调用了setState这种死循环的情况。

    然后通过markUpdateLaneFromFiberToRoot方法,向上递归更新fiber的lane,lane的更新很简单,就是将当前任务lane与之前lane进行二进制或运算叠加。

    image

    然后会根据任务类型以及当前线程所处的react执行阶段,去判断进行何种类型的更新:

    执行同步更新

    当任务的类型为同步任务,并且当前的js主线程空闲(没有正在执行的react任务时),会通过performSyncWorkOnRoot(root)方法开始执行同步任务。

    performSynWorkOnRoot(root)方法开始执行同步任务。
    performSyncWorkOnRoot里面主要做了两件事:

    • renderRootSync从根节点开始进行同步渲染任务
    • commitRoot执行commit流程

    image

    当任务类型为同步类型,但是js主线程非空闲时。会执行ensureRootIsScheduled方法:

    image

    ensureRootIsScheduled方法中,会先看到加入了新的任务后根节点任务优先级是否有变更,如果无变更,说明新的任务会被当前的schedule一同执行;如果有变更,则创建新的schedule,然后也是调用performSyncWorkOnRoot(root)方法开始执行同步任务。

    执行可中断更新

    当任务的类型不是同类型时,react也会执行ensureRootIsScheduled方法,因为是异步任务,最终会执行performConcurrentWorkOnRoot方法,去进行可中断的更新,下面会详细讲到。

    workLoop

    同步

    以同步更新为例,performSyncWorkOnRoot 会经过以下流程,performSyncWorkOnRoot——>renderRootSync——>workLoopSync。

    workLoopSync中,只要workInProgress(workInProgress fiber树中新创建的fiber节点) 不为null,就会一直循环,执行performUnitOfwork函数。

    image

    可中断

    可中断模式下,performConcurrentWorkOnRoot会执行以下过程:performConcurrentWorkOnRoot——>renderRootConcurrent——>workLoopConcurrent。

    相比较于workLoopSync,workLoopConcurrent在每一次对workInProgress执行performUnitOfWork前,会先判断以下shouldYield()的值。若为false则继续执行,若为true则中断执行。

    image

    performUnitOfWork

    最终无论是同步执行任务,还是可中断地执行任务,都会进入performUnitOfWork函数中。

    performUnitOfWork中会以fiber作为单元,进行协调过程。每次beginWork执行后都会更新workIngProgress,从而响应了上面workLoop的循环。

    直至fiber树便利完成后,workInProgress此时值为null,执行completeUnitOfWork函数。

    image

    beginWork

    beginWork是根据当前执行环境,封装调用了originalBeginWork函数:

    image

    originalBeginWork中,会根据workInProgress的tag属性,执行不同类型的react元素的更新函数。但是他们都大同小异,不论是tag是何种类型,更新函数最终都会去调用reconcileChildren函数。

    image

    以updateHostRoot为例,根据根fiber是否存在,去执行mountChildFibers或者reconcileChildren:

    image

    reconcileChildren做的事情就是react的另一核心之一diff过程,在下一篇文章中会详细讲。

    completeUnitOfWork

    当workInProgress为null时,也就是当前任务的fiber树遍历完之后,就进入到了completeUnitOfWork函数。

    经过了beginWork操作,workInProgress节点已经被打上了flags副作用标签。completeUnitOfWork方法中主要是逐层收集effects链,最终收集到root上,供接下来的commit阶段使用。

    completeUnitOfWork结束后,render阶段便结束了,后面就到了commit阶段。

    image

    scheduler

    实现帧空闲调度任务

    浏览器会在每一帧空闲时刻去执行react更新任务,那么空闲时刻去执行是如何实现的呢?我们很容易联想到一个api——————requestldleCallback。但由于requestldleCallback的兼容性问题以及react对应部分高优先级任务可能牺牲部分帧的需要,react通过自己实现了类似的功能代替了requestldleCallback。

    我们上面讲到执行可中断更新时,performConcurrentWorkOnRoot函数时通过scheduleCallback包裹起来的:
    scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null,root),
    )

    scheduleCallback函数是引用了packages/scheduler/src/Scheduler.js 路径下的unstable_scheduleCallback函数,我们来看一下这个函数,他会去按计划插入调度任务:
    image

    将任务插入了调度队列之后,会通过requestHostCallback函数去调度任务。

    来源:https://juejin.cn/post/7019254208830373902/

  • 相关阅读:
    hdu5894 hannnnah_j’s Biological Test(组合数取模)
    HDU5883 The Best Path(并查集+欧拉路)
    HDU5881 Tea(简单题)
    组合数取模
    codeforces703D Mishka and Interesting sum(区间偶数异或)
    upcoj2679 Binary Tree(思路题)
    upcoj2673 It Can Be Arranged(isap)
    atom编辑器适用
    python库pandas
    fabric Node SDK进行连接
  • 原文地址:https://www.cnblogs.com/huayang1995/p/15910753.html
Copyright © 2020-2023  润新知