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

2022/2/18 20:20:19

本文主要是介绍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/



这篇关于react17.x源码解析(2)——fiber树的构建与更新的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程