【React架构 - Fiber构造循环】

我们都知道在React中存在两大循环任务调度循环、Fiber构造循环,本文只要介绍的是其中的FIber构造循环,至于任务调度循环,有兴趣的可以查看写的这篇文章:【React源码 - 调度任务循环EventLoop】

本文主要从三个方面来介绍FIber构造的整个过程:

  1. 基础元素介绍
  2. 初次渲染
  3. 更新渲染

基础元素介绍

这里会简单列举一下待会儿会涉及的核心元素:

Fiber对象:

一个Fiber对象代表一个即将渲染或者已经渲染的组件(ReactElement),
一个组件可能对应两个fiber(current和WorkInProgress: 双缓冲树)

FIber结构如下:

export type Fiber = {|tag: WorkTag, // 表示 fiber 类型, 根据ReactElement组件的 type 进行生成, 在 react 内部共定义了25 种 tag.key: null | string, // 作为标识,对比可用elementType: any, // 一般来讲和ReactElement组件的 type 一致,div、span...type: any,stateNode: any, // 指向页面上的真实dom实例return: Fiber | null, // 指向父节点child: Fiber | null, // 指向子节点sibling: Fiber | null, // 指向下一个兄弟节点index: number, // fiber 在兄弟节点中的索引, 如果是单节点默认为 0.ref:| null| (((handle: mixed) => void) & { _stringRef: ?string, ... })| RefObject,pendingProps: any, // 从`ReactElement`对象传入的 props. 用于和`fiber.memoizedProps`比较可以得出属性是否变动memoizedProps: any, // 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中updateQueue: mixed, // 存储state更新的队列, 当前节点的state改动之后, 都会创建一个update对象添加到这个队列中.memoizedState: any, // 用于输出的state, 最终渲染所使用的statedependencies: Dependencies | null, // 该fiber节点所依赖的(contexts, events)等mode: TypeOfMode, // 二进制位Bitfield,继承至父节点,影响本fiber节点及其子树中所有节点. 与react应用的运行模式有关(有ConcurrentMode, BlockingMode, NoMode等选项).// Effect 副作用相关flags: Flags, // 标志位subtreeFlags: Flags, //替代16.x版本中的 firstEffect, nextEffect. 当设置了 enableNewReconciler=true才会启用deletions: Array<Fiber> | null, // 存储将要被删除的子节点. 当设置了 enableNewReconciler=true才会启用nextEffect: Fiber | null, // 单向链表, 指向下一个有副作用的fiber节点firstEffect: Fiber | null, // 指向副作用链表中的第一个fiber节点lastEffect: Fiber | null, // 指向副作用链表中的最后一个fiber节点// 优先级相关lanes: Lanes, // 本fiber节点的优先级childLanes: Lanes, // 子节点的优先级alternate: Fiber | null, // 指向内存中的另一个fiber, 每个被更新过fiber节点在内存中都是成对出现(current和workInProgress)// 性能统计相关(开启enableProfilerTimer后才会统计)// react-dev-tool会根据这些时间统计来评估性能actualDuration?: number, // 本次更新过程, 本节点以及子树所消耗的总时间actualStartTime?: number, // 标记本fiber节点开始构建的时间selfBaseDuration?: number, // 用于最近一次生成本fiber节点所消耗的时间treeBaseDuration?: number, // 生成子树所消耗的时间的总和
|};

由于上面的片段无法添加链接,所以对于有链接的字段说明这里在列举一下:

  • fiber.tag: 表示 fiber 类型, 根据ReactElement组件的 type 进行生成, 在 react 内部共定义了25 种 tag.
  • fiber.subtreeFlags: 替代 16.x 版本中的 firstEffect, nextEffect. 默认未开启, 当设置了enableNewReconciler=true 才会启用, 本系列只跟踪稳定版的代码, 未来版本不会深入解读, 使用示例见源码.
  • fiber.deletions: 存储将要被删除的子节点. 默认未开启, 当设置了enableNewReconciler=true 才会启用, 本系列只跟踪稳定版的代码, 未来版本不会深入解读, 使用示例见源码.

ReactDOM(Blocking)Root对象

属于react-dom包, 该对象暴露有render,unmount方法, 通过调用该实例的render方法, 可以引导 react 应用的启动.

fiberRoot对象

属于react-reconciler包, 作为react-reconciler在运行过程中的全局上下文, 保存 fiber 构建过程中所依赖的全局状态.其大部分实例变量用来存储fiber 构造循环过程的各种状态.react 应用内部, 可以根据这些实例变量的值, 控制执行逻辑.

HostRootFiber对象

属于react-reconciler包, 这是 react 应用中的第一个 Fiber 对象, 是 Fiber 树的根节点, 节点的类型是HostRoot.

这 3 个对象是 react 体系得以运行的基本保障, 一经创建大多数场景不会再销毁(除非卸载整个应用root.unmount()).

副作用队列

effect链表(环形链式队列): 存储有副作用的子节点, 构成该队列的元素是fiber对象
fiber.nextEffect: 单向链表, 指向下一个有副作用的 fiber 节点.
fiber.firstEffect: 指向副作用链表中的第一个 fiber 节点.
fiber.lastEffect: 指向副作用链表中的最后一个 fiber 节点.
在这里插入图片描述

在fiber构造的completeWork中副作用上移会构建,最后绑定到root并通过commitRoot进入render阶段

状态更新队列

updateQueue链表(环形链式队列): 存储将要更新的状态, 构成该队列的元素是update对象,
fiber.updateQueue.pending: 存储state更新的队列(链式队列), class类型节点的state改动之后, 都会创建一个update对象添加到这个队列中. 由于此队列是一个环形队列, 为了方便添加新元素和快速拿到队首元素, 所以pending指针指向了队列中最后一个元素.
在这里插入图片描述

双缓冲树

在全局变量中有workInProgress, 还有不少以workInProgress来命名的变量. workInProgress的应用实际上就是React的双缓冲技术(double buffering).
在上面我们梳理了ReactElement, Fiber, DOM三者的关系, fiber树的构造过程, 就是把ReactElement转换成fiber树的过程. 在这个过程中, 内存里会同时存在 2 棵fiber树:

  • 一棵:代表当前界面的fiber树(已经被展示出来, 挂载到fiberRoot.current上). 如果是初次构造(初始化渲染), 页面还没有渲染, 此时界面对应的 fiber 树为空(fiberRoot.current = null).
  • 一棵:正在构造的fiber树(即将展示出来, 挂载到HostRootFiber.alternate上, 正在构造的节点称为workInProgress). 当构造完成之后, 重新渲染页面, 最后切换fiberRoot.current = workInProgress, 使得fiberRoot.current重新指向代表当前界面的fiber树.

用图来表述double buffering的概念如下:

  • 构造过程中, fiberRoot.current指向当前界面对应的fiber树.
    在这里插入图片描述
  • 构造完成并渲染, 切换fiberRoot.current指针, 使其继续指向当前界面对应的fiber树(原来代表界面的 fiber 树, 变成了内存中).
    在这里插入图片描述
    通过FiberRoot.current来指向当前页面展示的fiber树,通过FiberRoot.current.alternate来指向内存中正在构建的fiber树,当构造完成,页面更新的时候只需要改变current和alternate的指向就好。

current、alternate只是指针

优先级

主要是三种优先级:update优先级(update.lane)、渲染优先级(renderLanes)、fiber优先级(fiber.lane).这些优先级在fiber构造中会频繁使用,比如在beginWork中会根据优先级来判断是否需要更新来进行节点复用等

update优先级

在应用初始化(updateContainer)或者发起组件更新(setState), 创建update.lane的逻辑都是一样的, 都是根据当前时间, 创建一个 update 优先级.最后通过scheduleUpdateOnFiber(current, lane, eventTime);函数, 把update.lane正式带入到了输入阶段.

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); // 根据当前时间, 创建一个update优先级const update = createUpdate(eventTime, lane); // lane被用于创建update对象update.payload = { element };enqueueUpdate(current, update);scheduleUpdateOnFiber(current, lane, eventTime);return lane;
}const classComponentUpdater = {isMounted,enqueueSetState(inst, payload, callback) {const fiber = getInstance(inst);const eventTime = requestEventTime(); // 根据当前时间, 创建一个update优先级const lane = requestUpdateLane(fiber); // lane被用于创建update对象const update = createUpdate(eventTime, lane);update.payload = payload;enqueueUpdate(fiber, update);scheduleUpdateOnFiber(fiber, lane, eventTime);},
};
渲染优先级

这是一个全局概念, 每一次render之前, 首先要确定本次render的优先级. 具体对应到源码如下:

// ...省略无关代码
function performSyncWorkOnRoot(root) {let lanes;let exitStatus;// 获取本次`render`的优先级lanes = getNextLanes(root, lanes);exitStatus = renderRootSync(root, lanes);
}
// ...省略无关代码
function performConcurrentWorkOnRoot(root) {// 获取本次`render`的优先级let lanes = getNextLanes(root,root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,);if (lanes === NoLanes) {return null;}let exitStatus = renderRootConcurrent(root, lanes);
}

在正式render之前, 都会调用getNextLanes获取一个优先级

// ...省略部分代码
export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {// 1. check是否有等待中的lanesconst pendingLanes = root.pendingLanes;if (pendingLanes === NoLanes) {return_highestLanePriority = NoLanePriority;return NoLanes;}let nextLanes = NoLanes;let nextLanePriority = NoLanePriority;const expiredLanes = root.expiredLanes;const suspendedLanes = root.suspendedLanes;const pingedLanes = root.pingedLanes;// 2. check是否有已过期的lanesif (expiredLanes !== NoLanes) {nextLanes = expiredLanes;nextLanePriority = return_highestLanePriority = SyncLanePriority;} else {const nonIdlePendingLanes = pendingLanes & NonIdleLanes;if (nonIdlePendingLanes !== NoLanes) {// 非Idle任务 ...} else {// Idle任务 ...}}if (nextLanes === NoLanes) {return NoLanes;}return nextLanes;
}

NoLanes是最高优先级,React中值越小,优先级越大

getNextLanes会根据fiberRoot对象上的属性(expiredLanes, suspendedLanes, pingedLanes等), 确定出当前最紧急的lanes.
此处返回的lanes会作为全局渲染的优先级, 用于fiber树构造过程中(beginWork). 针对fiber对象或update对象, 只要它们的优先级(如: fiber.lanes和update.lane)比渲染优先级低, 都将会被忽略.

fiber优先级

上面介绍过fiber对象的数据结构. 其中有 2 个属性与优先级相关: 默认都是最高优先级NoLanes

  • fiber.lanes: 代表本节点的优先级
  • fiber.childLanes: 代表子节点的优先级
    在fiber树构造过程中beginWork对比更新时会 使用全局的渲染优先级(renderLanes)和fiber.lanes判断fiber节点是否更新(源码地址).
    1、如果全局的渲染优先级renderLanes不包括fiber.lanes, 证明该fiber节点没有更新, 可以复用.(bailoutOnAlreadyFinishedWork)
    2、如果不能复用, 进入创建fiber阶段.

栈帧管理

我们都知道React有可中断机制,当被更高级任务或者需要让出主线程的时候当前任务会被中断,等下一次调用的继续执行,而还原上一次的任务,就需要栈帧stack,这里面保存了构造fiber时所需要的全局变量,fiber树构造是一个独立的过程, 需要独立的一组全局变量, 在React内部把这一个独立的过程封装为一个栈帧stack(简单来说就是每次构造都需要独立的空间),当我们需要恢复的时候就使用栈帧中保存的变量。所以在进行fiber树构造之前, 如果不需要恢复上一次构造进度, 都会刷新栈帧(源码在prepareFreshStack 函数)

function prepareFreshStack(root: FiberRoot, lanes: Lanes) {// 重置FiberRoot对象上的属性root.finishedWork = null;root.finishedLanes = NoLanes;const timeoutHandle = root.timeoutHandle;if (timeoutHandle !== noTimeout) {root.timeoutHandle = noTimeout;cancelTimeout(timeoutHandle);}if (workInProgress !== null) {let interruptedWork = workInProgress.return;while (interruptedWork !== null) {unwindInterruptedWork(interruptedWork);interruptedWork = interruptedWork.return;}}// 重置全局变量workInProgressRoot = root;workInProgress = createWorkInProgress(root.current, null); // 给HostRootFiber对象创建一个alternate, 并将其设置成全局 workInProgressworkInProgressRootRenderLanes =subtreeRenderLanes =workInProgressRootIncludedLanes =lanes;workInProgressRootExitStatus = RootIncomplete;workInProgressRootFatalError = null;workInProgressRootSkippedLanes = NoLanes;workInProgressRootUpdatedLanes = NoLanes;workInProgressRootPingedLanes = NoLanes;
}

prepareFreshStack刷新栈帧,主要就是放弃上一次fiber构造未完成的数据讲数据初始化。

初次渲染

流程示意图如下:
在这里插入图片描述
准确的说上面的流程中ensureRootIsScheduled在初次渲染的时候不会执行,链路是:updateContainer -> scheduleUpdateOnFiber -> performSyncWorkOnRoot -> renderRootSync -> performUnitOfWork(beginWork <=> completeWork) -> commitRoot(render阶段),下面也是根据这个调用关系来说明整个fiber构造过程的

updateContainer

从代码中看,可分为一下几个步骤:
1、获取Lane优先级
2、根据优先级创建update对象
3、将update对象添加到updateQueue环形链表中
4、进入scheduleUpdateOnFiber入口进行调度

// ... 省略了部分代码
export function updateContainer(element: ReactNodeList,container: OpaqueRoot,parentComponent: ?React$Component<any, any>,callback: ?Function,
): Lane {// 获取当前时间戳const current = container.current;const eventTime = requestEventTime();// 1. 创建一个优先级变量(车道模型)const lane = requestUpdateLane(current);// 2. 根据车道优先级, 创建update对象, 并加入fiber.updateQueue.pending队列const update = createUpdate(eventTime, lane);update.payload = { element };callback = callback === undefined ? null : callback;if (callback !== null) {update.callback = callback;}enqueueUpdate(current, update);// 3. 进入reconciler运作流程中的`输入`环节scheduleUpdateOnFiber(current, lane, eventTime);return lane;
}

scheduleUpdateOnFiber

在初次渲染中,不会调用ensureRootIsScheduled进入Schedule进行调度,直接通过performSyncWorkOnRoot进行fiber构造,markUpdateLaneFromFiberToRoot函数在更新渲染中才会有用,它找出了fiber树中受到本次update影响的所有节点, 并设置这些节点的fiber.lanes或fiber.childLanes以备fiber树构造阶段使用.

// ...省略部分代码
export function scheduleUpdateOnFiber(fiber: Fiber,lane: Lane,eventTime: number,
) {// 标记优先级const root = markUpdateLaneFromFiberToRoot(fiber, lane);if (lane === SyncLane) {if ((executionContext & LegacyUnbatchedContext) !== NoContext &&(executionContext & (RenderContext | CommitContext)) === NoContext) {// 首次渲染, 直接进行`fiber构造`performSyncWorkOnRoot(root);}// ...}
}

performSyncWorkOnRoot

1、通过getNextLanes获取渲染优先级,初次返回NoLanes
2、将root(workInProgress)和渲染优先级传入renderRootSync,进入workLoop进行fiber构造
3、构造完成之后将最新的fiber树挂载在root.finshedWork上,通过commitRoot进入render阶段

function performSyncWorkOnRoot(root) {let lanes;let exitStatus;if (root === workInProgressRoot &&includesSomeLane(root.expiredLanes, workInProgressRootRenderLanes)) {// 初次构造时(因为root=fiberRoot, workInProgressRoot=null), 所以不会进入} else {// 1. 获取本次render的优先级, 初次构造返回 NoLaneslanes = getNextLanes(root, NoLanes);// 2. 从root节点开始, 至上而下更新exitStatus = renderRootSync(root, lanes);}// 将最新的fiber树挂载到root.finishedWork节点上const finishedWork: Fiber = (root.current.alternate: any);root.finishedWork = finishedWork;root.finishedLanes = lanes;// 进入commit阶段commitRoot(root);// ...后面的内容本节不讨论
}

renderRootSync

1、当fiberRoot和update.lane变动时,刷新栈帧,丢弃上次fiber构造数据(可以查看上方的栈帧管理部分)
2、执行workLoopSync进入到fiber构造循环

这里为什么使用do…while来执行,除了都知道的该语法会至少执行一次,不管while是否满足,查找了一下,没找到相关资料,只知道在c/c++中为了保持宏函数实现的时候语意和我们所写的代码一致,防止大括号以及分号的干扰,使用do…while,这里有知道的小伙伴,可以评论滴滴我

3、构造完成之后,重制全局变量,表示render阶段结束,进入commit阶段

function renderRootSync(root: FiberRoot, lanes: Lanes) {const prevExecutionContext = executionContext;executionContext |= RenderContext;// 如果fiberRoot变动, 或者update.lane变动, 都会刷新栈帧, 丢弃上一次渲染进度if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {// 刷新栈帧, legacy模式下都会进入prepareFreshStack(root, lanes);}do {try {workLoopSync();break;} catch (thrownValue) {handleError(root, thrownValue);}} while (true);executionContext = prevExecutionContext;// 重置全局变量, 表明render结束workInProgressRoot = null;workInProgressRootRenderLanes = NoLanes;return workInProgressRootExitStatus;
}

performUnitOfWork

由于workLoopSync比较简单,就当fiber节点不为空时,执行performUnitOfWork函数,所以统一在这里源码说明

function workLoopSync() {while (workInProgress !== null) {performUnitOfWork(workInProgress);}
}

performUnitOfWork函数主要就是先执行beginWork(深度优先遍历),当遍历完当前fiber节点的所以子节点之后就进入到completeUnitOfWork(completeWork阶段)

都知道在React15之前是使用的递归来遍历所有节点,但是由于递归存在问题,不可中断会阻塞关键流程导致卡顿,所以在16之后就使用了循环来代替递归,就是下面会介绍的beginWork、completeWork阶段,可以理解为beginWork是递的阶段,从上往下探寻,completeWork就是归的阶段,从下往上回溯

从源码中能看出:
1、执行beginWork开始深度优先往下遍历
2、将本次构建的props保存在memoizedProps中,方便下次fiber更新的时候进行对比判断是否复用
3、遍历完成则进入completeWork阶段

// ... 省略部分无关代码
function performUnitOfWork(unitOfWork: Fiber): void {// unitOfWork就是被传入的workInProgressconst current = unitOfWork.alternate;let next;next = beginWork(current, unitOfWork, subtreeRenderLanes);unitOfWork.memoizedProps = unitOfWork.pendingProps;if (next === null) {// 如果没有派生出新的节点, 则进入completeWork阶段, 传入的是当前unitOfWorkcompleteUnitOfWork(unitOfWork);} else {workInProgress = next;}
}

beginWork

beginWork接受三个参数:

  • current: Fiber | null: // 当前页面展示的dom对应的fiber
  • workInProgress: Fiber, // 正在构造的fiber
  • renderLanes: Lanes, // 渲染优先级

主要是根据workInProgress.tag来对不同的fiber进行操作,大致逻辑是

  • 根据JSX转换的 ReactElement对象调用reconcileChildren来创建当前fiber所有的子fiber节点, 最终构造出fiber树形结构(设置return和sibling指针)
  • 设置fiber.flags(二进制形式变量, 用来标记 fiber节点 的增,删,改状态, 等待completeWork阶段处理)
  • 将workInProgress指针往下移动到子fiber节点
  • 返回当前fiber的子fiber节点,如果没有子节点则表示遍历完成,退出beginWork并进入completeWork阶段。
function beginWork(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,
): Fiber | null {// 更新优先级const updateLanes = workInProgress.lanes;if (current !== null) {// update逻辑, 首次render不会进入} else {didReceiveUpdate = false;}// 1. 设置workInProgress优先级为NoLanes(最高优先级)workInProgress.lanes = NoLanes;// 2. 根据workInProgress节点的类型, 用不同的方法派生出子节点switch (workInProgress.tag // 只保留了本例使用到的case) {case ClassComponent: {const Component = workInProgress.type;const unresolvedProps = workInProgress.pendingProps;const resolvedProps =workInProgress.elementType === Component? unresolvedProps: resolveDefaultProps(Component, unresolvedProps);return updateClassComponent(current,workInProgress,Component,resolvedProps,renderLanes,);}case HostRoot:return updateHostRoot(current, workInProgress, renderLanes);case HostComponent:return updateHostComponent(current, workInProgress, renderLanes);case HostText:return updateHostText(current, workInProgress);case Fragment:return updateFragment(current, workInProgress, renderLanes);}
}

从上面能看出对于不同类型的组件都进行了处理,这里简单列举一下HostRoot、HostComponent组件时的处理

HostRoot

fiber树的根节点是HostRootFiber节点, 所以第一次进入beginWork会调用updateHostRoot(current, workInProgress, renderLanes),在updateHostRoot函数中主要是下面步骤:

  1. 计算workInProgress.memoizedState
  2. 获取下级ReactElement对象
  3. 调用reconcileChildren生成下级fiber节点
// 省略与本节无关代码
function updateHostRoot(current, workInProgress, renderLanes) {// 1. 状态计算, 更新整合到 workInProgress.memoizedState中来const updateQueue = workInProgress.updateQueue;const nextProps = workInProgress.pendingProps;const prevState = workInProgress.memoizedState;const prevChildren = prevState !== null ? prevState.element : null;cloneUpdateQueue(current, workInProgress);// 遍历updateQueue.shared.pending, 提取有足够优先级的update对象, 计算出最终的状态 workInProgress.memoizedStateprocessUpdateQueue(workInProgress, nextProps, null, renderLanes);const nextState = workInProgress.memoizedState;// 2. 获取下级`ReactElement`对象const nextChildren = nextState.element;const root: FiberRoot = workInProgress.stateNode;if (root.hydrate && enterHydrationState(workInProgress)) {// ...服务端渲染相关, 此处省略} else {// 3. 根据`ReactElement`对象, 调用`reconcileChildren`生成`Fiber`子节点(只生成`次级子节点`)reconcileChildren(current, workInProgress, nextChildren, renderLanes);}return workInProgress.child;
}
updateHostComponent

当workInProgress.tag为HostComponent时就会进入updateHostComponent

HostComponent类型就是原生普通 DOM 标签类型的节点(如div,span,p…),

由于HostComponent是原声组件,是无状态组件,不需要计算memoizedState,只需要根据更新计算最新的props(nextProps)即可。

  1. 获取当前最新以及上次更新的props(初次渲染没有prevProps)
  2. 获取下级ReactElement对象
  3. 对子节点处理
    3.1 只有一共文本节点,则不需要构建fiber
    3.2 设置子节点的flag
  4. 根据ReactElement对象, 调用reconcileChildren生成Fiber子节点(只生成次级子节点)
  5. 返回子节点
// ...省略部分无关代码
function updateHostComponent(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,
) {// 1. 状态计算, 由于HostComponent是无状态组件, 所以只需要收集 nextProps即可, 它没有 memoizedStateconst type = workInProgress.type;const nextProps = workInProgress.pendingProps;const prevProps = current !== null ? current.memoizedProps : null;// 2. 获取下级`ReactElement`对象let nextChildren = nextProps.children;const isDirectTextChild = shouldSetTextContent(type, nextProps);if (isDirectTextChild) {// 如果子节点只有一个文本节点, 不用再创建一个HostText类型的fibernextChildren = null;} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {// 特殊操作需要设置fiber.flagsworkInProgress.flags |= ContentReset;}// 特殊操作需要设置fiber.flagsmarkRef(current, workInProgress);// 3. 根据`ReactElement`对象, 调用`reconcileChildren`生成`Fiber`子节点(只生成`次级子节点`)reconcileChildren(current, workInProgress, nextChildren, renderLanes);return workInProgress.child;
}

当beginWork执行到fiber节点到叶子节点之后,返回到子节点为null,这时候就会退出beginWork进入到completeWork阶段。
在这里插入图片描述
在上面performUnitOfWork函数中提到,当返回的子节点即next为空,就会调用completeUnitOfWork进入completeWork阶段

function performUnitOfWork(unitOfWork: Fiber): void {// unitOfWork就是被传入的workInProgressconst current = unitOfWork.alternate;let next;next = beginWork(current, unitOfWork, subtreeRenderLanes);unitOfWork.memoizedProps = unitOfWork.pendingProps;if (next === null) {// 如果没有派生出新的节点, 则进入completeWork阶段, 传入的是当前unitOfWorkcompleteUnitOfWork(unitOfWork);} else {workInProgress = next;}
}

completeUnitOfWork

在completeUnitOfWork函数中主要是这些处理:

  1. 调用completeWork进入阶段
  2. 当前fiber节点是否有派生节点(兄弟、子节点),如果有则回到阶段,再一次执行beginWork
  3. 调用resetChildLanes重置子节点优先级
  4. 将当前fiber的子节点副作用队列添加到父节点(firstEffect、lasrEffect)
  5. 判断当前fiber节点是否有副作用,如果有则加入到队列中并添加到父节点(根据beginWork设置的flag判断)
  6. 移动workInProgress指针到sibling节点或者父节点

第4步中只是将当前fiber的子节点副作用队列加入到父节点,当前fiber节点的本身副作用要在第5步的时候根据flags来判断之后添加到副作用队列末尾即lastEffect后面,最后回溯到root节点之后就是本次fiber更新需要调整的所有副作用队列(循环链表)由root.firstEffect指向队列开始,由root.lastEffect来指向队列结尾

在fiber中存在两个循环队列:updateQueue、Effects副作用队列,可能有时候不能很好的区分,在这里简单介绍一下:updateQueue就是状态更新队列,里面保存了本次更新变化的状态,比如setState、useState等。而Effects副作用队列则是表示fiber节点需要具体做什么操作,比如增、删、改由fiber.flag决定,更详细的可以查看上面基础元素介绍中对副作用队列、更新队列的介绍。

function completeUnitOfWork(unitOfWork: Fiber): void {let completedWork = unitOfWork;// 外层循环控制并移动指针(`workInProgress`,`completedWork`等)do {const current = completedWork.alternate;const returnFiber = completedWork.return;if ((completedWork.flags & Incomplete) === NoFlags) {let next;// 1. 处理Fiber节点, 会调用渲染器(调用react-dom包, 关联Fiber节点和dom对象, 绑定事件等)next = completeWork(current, completedWork, subtreeRenderLanes); // 处理单个节点if (next !== null) {// 如果派生出其他的子节点, 则回到`beginWork`阶段进行处理workInProgress = next;return;}// 重置子节点的优先级resetChildLanes(completedWork);if (returnFiber !== null &&(returnFiber.flags & Incomplete) === NoFlags) {// 2. 收集当前Fiber节点以及其子树的副作用effects// 2.1 把子节点的副作用队列添加到父节点上if (returnFiber.firstEffect === null) {returnFiber.firstEffect = completedWork.firstEffect;}if (completedWork.lastEffect !== null) {if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = completedWork.firstEffect;}returnFiber.lastEffect = completedWork.lastEffect;}// 2.2 如果当前fiber节点有副作用, 将其添加到子节点的副作用队列之后.const flags = completedWork.flags;if (flags > PerformedWork) {// PerformedWork是提供给 React DevTools读取的, 所以略过PerformedWorkif (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = completedWork;} else {returnFiber.firstEffect = completedWork;}returnFiber.lastEffect = completedWork;}}} else {// 异常处理, 本节不讨论}const siblingFiber = completedWork.sibling;if (siblingFiber !== null) {// 如果有兄弟节点, 返回之后再次进入`beginWork`阶段workInProgress = siblingFiber;return;}// 移动指针, 指向下一个节点completedWork = returnFiber;workInProgress = completedWork;} while (completedWork !== null);// 已回溯到根节点, 设置workInProgressRootExitStatus = RootCompletedif (workInProgressRootExitStatus === RootIncomplete) {workInProgressRootExitStatus = RootCompleted;}
}

completeWork

completeWork就是上面多次说到的归阶段,在这里面主要是(参考HostComponent中的处理逻辑):

  1. 根据当前fiber状态DOM实例,并将dom绑定到fiber的stateNode上
  2. 处理DOM节点的属性、事件等
  3. 设置flags来标记副作用
function completeWork(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,
): Fiber | null {const newProps = workInProgress.pendingProps;switch (workInProgress.tag) {case ClassComponent: {// Class类型不做处理return null;}case HostRoot: {const fiberRoot = (workInProgress.stateNode: FiberRoot);if (fiberRoot.pendingContext) {fiberRoot.context = fiberRoot.pendingContext;fiberRoot.pendingContext = null;}if (current === null || current.child === null) {// 设置fiber.flags标记workInProgress.flags |= Snapshot;}return null;}case HostComponent: {popHostContext(workInProgress);const rootContainerInstance = getRootHostContainer();const type = workInProgress.type;if (current !== null && workInProgress.stateNode != null) {// update逻辑, 初次render不会进入} else {const currentHostContext = getHostContext();// 1. 创建DOM对象const instance = createInstance(type,newProps,rootContainerInstance,currentHostContext,workInProgress,);// 2. 把子树中的DOM对象append到本节点的DOM对象之后appendAllChildren(instance, workInProgress, false, false);// 设置stateNode属性, 指向DOM对象workInProgress.stateNode = instance;if (// 3. 设置DOM对象的属性, 绑定事件等finalizeInitialChildren(instance,type,newProps,rootContainerInstance,currentHostContext,)) {// 设置fiber.flags标记(Update)markUpdate(workInProgress);}if (workInProgress.ref !== null) {// 设置fiber.flags标记(Ref)markRef(workInProgress);}return null;}}
}

更新渲染

上面详细介绍了在页面初次渲染需要进行的逻辑,而对比更新相对于初次渲染,主要就是多了调度和对比的逻辑,从这个示意图也能看出来,通过入口函数scheduleUpdateOnFIber会通过ensureRootIsScheduled进入到Schedule调度器中创建task等待调度(初次渲染不会),在调度中主要是另一个循环,大家可以查看【React源码 - 调度任务循环EventLoop】来了解任务调度循环的逻辑,在这里我们就不再赘述了,这里主要讲一下对比更新和初次渲染不一样的逻辑:主要逻辑也是在beginWork和completeWork阶段

beginWork

function beginWork(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,
): Fiber | null {const updateLanes = workInProgress.lanes;if (current !== null) {// 进入对比const oldProps = current.memoizedProps;const newProps = workInProgress.pendingProps;if (oldProps !== newProps ||hasLegacyContextChanged() ||(__DEV__ ? workInProgress.type !== current.type : false)) {didReceiveUpdate = true;} else if (!includesSomeLane(renderLanes, updateLanes)) {// 当前渲染优先级renderLanes不包括fiber.lanes, 表明当前fiber节点无需更新didReceiveUpdate = false;switch (workInProgress.tag// switch 语句中包括 context相关逻辑, 本节暂不讨论(不影响分析fiber树构造)) {}// 当前fiber节点无需更新, 调用bailoutOnAlreadyFinishedWork循环检测子节点是否需要更新return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);}}// 余下逻辑与初次创建共用// 1. 设置workInProgress优先级为NoLanes(最高优先级)workInProgress.lanes = NoLanes;// 2. 根据workInProgress节点的类型, 用不同的方法派生出子节点switch (workInProgress.tag // 只列出部分case) {case ClassComponent: {const Component = workInProgress.type;const unresolvedProps = workInProgress.pendingProps;const resolvedProps =workInProgress.elementType === Component? unresolvedProps: resolveDefaultProps(Component, unresolvedProps);return updateClassComponent(current,workInProgress,Component,resolvedProps,renderLanes,);}case HostRoot:return updateHostRoot(current, workInProgress, renderLanes);case HostComponent:return updateHostComponent(current, workInProgress, renderLanes);case HostText:return updateHostText(current, workInProgress);case Fragment:return updateFragment(current, workInProgress, renderLanes);}
}

从代码中可以看出当current != null时,进入对比更新,会判断props和上下文context是否变化,来设置需要更新节点的变量didReceiveUpdate最后会执行bailoutOnAlreadyFinishedWork函数,用于判断子树节点是否完全复用, 如果可以复用, 则会略过 fiber 树构造.

bailoutOnAlreadyFinishedWork

bail out英文短语翻译为解救, 纾困, 在源码中, bailout用于判断子树节点是否完全复用, 如果可以复用, 则会略过 fiber 树构造.

  • !includesSomeLane(renderLanes, updateLanes)这个判断分支, 包含了渲染优先级和update优先级的比较(详情可以回顾上方基础元素介绍中优先级相关解读), 如果当前节点无需更新, 则会进入bailout逻辑.
  • 最后会调用bailoutOnAlreadyFinishedWork:
    如果同时满足!includesSomeLane(renderLanes, workInProgress.childLanes), 表明该 fiber 节点及其子树都无需更新, 可直接进入回溯阶段(completeUnitOfWork)
    如果不满足!includesSomeLane(renderLanes, workInProgress.childLanes), 意味着子节点需要更新, 通过cloneChildFibers来clone并返回子节点.

cloneChildFibers内部调用createWorkInProgress, 在构造fiber节点时会优先复用workInProgress.alternate(不开辟新的内存空间,即页面上存在的fiber节点), 否则才会创建新的fiber对象.

// 省略部分无关代码
function bailoutOnAlreadyFinishedWork(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,
): Fiber | null {if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {// 渲染优先级不包括 workInProgress.childLanes, 表明子节点也无需更新. 返回null, 直接进入回溯阶段.return null;} else {// 本fiber虽然不用更新, 但是子节点需要更新. clone并返回子节点cloneChildFibers(current, workInProgress);return workInProgress.child;}
}
updatexxx逻辑

如果没有满足上述条件,则会进入到根据workInProgress.tag来进行组件更新,updateXXX函数(如: updateHostRoot, updateClassComponent 等)的主干逻辑与初次构造过程完全一致, 总的目的是为了向下生成子节点, 并在这个过程中调用reconcileChildren调和函数, 只要fiber节点有副作用, 就会把特殊操作设置到fiber.flags(如:节点ref,class组件的生命周期,function组件的hook,节点删除等).
对比更新过程的不同之处:

  • bailoutOnAlreadyFinishedWork
    对比更新时如果遇到当前节点无需更新(如: class类型的节点且shouldComponentUpdate返回false), 会再次进入bailout逻辑.
  • reconcileChildren调和函数
    1、调和函数是updateXXX函数中的一项重要逻辑, 它的作用是向下生成子节点, 并设置fiber.flags.
    2、初次创建时fiber节点没有比较对象, 所以在向下生成子节点的时候没有任何多余的逻辑, 只管创建就行.
    3、对比更新时需要把ReactElement对象与旧fiber对象进行比较, 来判断是否需要复用旧fiber对象.diff算法

本节的重点是fiber树构造, 在对比更新过程中reconcileChildren()函数实现的diff算法十分重要, 但是它只是处于算法层面, 对于diff算法的实现,后续单独出篇文章说明,大家也可以查阅其他资料了解

总的来说,在对比更新阶段reconcileChildren调和函数主要就是

  • 设置flag,给新增,移动,和删除节点设置fiber.flags(新增,移动: Placement, 删除: Deletion)
  • 如果是需要删除的fiber, 除了自身打上Deletion之外, 还要将其添加到父节点的effects链表中(正常副作用队列的处理是在completeWork函数, 但是该节点(被删除)会脱离fiber树, 不会再进入completeWork阶段, 所以在beginWork阶段提前加入副作用队列).

completeWork

对比阶段的completeWork和初次渲染逻辑一致,都是根据beginWork生成的fiber生成并绑定dom,并上移副作用列表,下面以updateHostComponent为例

// ...省略无关代码
function completeWork(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,
): Fiber | null {const newProps = workInProgress.pendingProps;switch (workInProgress.tag) {case HostComponent: {// 非文本节点popHostContext(workInProgress);const rootContainerInstance = getRootHostContainer();const type = workInProgress.type;if (current !== null && workInProgress.stateNode != null) {// 处理改动updateHostComponent(current,workInProgress,type,newProps,rootContainerInstance,);if (current.ref !== workInProgress.ref) {markRef(workInProgress);}} else {// ...省略无关代码}return null;}case HostText: {// 文本节点const newText = newProps;if (current && workInProgress.stateNode != null) {const oldText = current.memoizedProps;// 处理改动updateHostText(current, workInProgress, oldText, newText);} else {// ...省略无关代码}return null;}}
}

当current !== null && workInProgress.stateNode != null则进入对比更新,会调用updateHostComponent,在里面当props发生改变的时候会通过markUpdate来讲workInProgress.flag设置为Update然后等待commit阶段处理

updateHostComponent = function (current: Fiber,workInProgress: Fiber,type: Type,newProps: Props,rootContainerInstance: Container,
) {const oldProps = current.memoizedProps;if (oldProps === newProps) {return;}const instance: Instance = workInProgress.stateNode;const currentHostContext = getHostContext();const updatePayload = prepareUpdate(instance,type,oldProps,newProps,rootContainerInstance,currentHostContext,);workInProgress.updateQueue = (updatePayload: any);// 如果有属性变动, 设置fiber.flags |= Update, 等待`commit`阶段的处理if (updatePayload) {markUpdate(workInProgress);}
};function markUpdate(workInProgress: Fiber) {// Tag the fiber with an update effect. This turns a Placement into// a PlacementAndUpdate. // export const Update = 0b000000000000000100;workInProgress.flags |= Update;
}

这里的Update是定义的常量,详细的可查看源码定义

commitRoot

当fiber构造完成,生成了一颗完整的fiber树表示render阶段完成,通过commitRoot(root)将整个fiber树传入进入到render阶段。这里整颗fiber的结构可以查看上面的基础元素介绍的FIber对象内容。

在React中主要有三部分调度Scheduler、协调Reconciler、渲染Render,我们将其分为两个阶段render、commit。其中Scheduler、Reconciler属于render阶段,表示在构造fiber树以及dom实例,这个阶段是异步可中断的。而Render属于commit阶段,表示将完成的fiber树交给react-dom来进行渲染到页面的操作,这阶段是不可中断的,所以常说的时间分片、可中断都是针对render阶段的,因为渲染是更新真实DOM用户感知界面阶段,一旦开始就不能结束,直到渲染完成。

结尾

本文只是从源码逻辑来介绍FIber构造的整个流程以及思路,具体的渲染图释,可以查看这篇文章:图解React

本文也是根据这些文章学习进行梳理在自己理解的基础上书写的,如有问题,还请指正。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/521562.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Chrome浏览器好用的几个扩展程序

Chrome好用的扩展程序 背景目的介绍JsonHandle例子未完待续。。。。。。 背景 偶然在往上看到Chrome有很多好用的扩展程序&#xff0c;比较好用&#xff0c;因此记录下比较实用的扩展程序。 目的 记录Chrome浏览器好用的插件。 介绍 JsonHandle下载以及无法扩展插件的解决…

九州金榜|家庭教育如何培养孩子独立能力?

在家庭教育中&#xff0c;要怎么样培养孩子的独立能力&#xff1f;很多家长可能会对此比较疑惑。因为现在的孩子从出生家长就会为孩子规划好了一切&#xff0c;孩子只要按部就班的做就好了&#xff0c;所以导致很多孩子没有独立能力&#xff0c;那家长如何培养孩子独立能力呢&a…

VSCode如何设置放大缩小文字的问题

输入栏中输入mouseWheelZoom,选中即可

2024年大语言模型的微调

一个LLM的生命周期包含多个步骤&#xff0c;下面将讨论这个周期中最活跃、最密集的部分之一 -- fine-tuning(微调)过程。 LLM的生命周期 下面展示了LLM的生命周期&#xff1a; 愿景和范围&#xff1a;首先需要定义项目的愿景&#xff0c;你想让你的LLM作为一个更加通用的工具…

ERP实施顾问面试题目

02什么是BOM和ECN&#xff1f;它们的完整英文拼写是什么&#xff1f;什么是替代料&#xff1f;&#xff08;10分&#xff09; BOM物料清单是英文Bill of Material的简写&#xff1b;ECN工程变更通知单是英文Engineering Change Notice的简写&#xff1b;替代料&#xff1a;由于…

Linux - 反弹Shell

概念 Shell 估计大家都不陌生&#xff0c;简单来说&#xff0c;就是实现用户命令的接口&#xff0c;通过这个接口我们就能实现对计算机的控制&#xff0c;比如我们常见的 ssh 就是执行的 Shell 命令实现对远程对服务器的控制。 那反弹 Shell &#xff08; Reverse Shell&…

震撼开源!李开复零一万物90亿参数Yi模型驾到,编码数学全能助力,你敢来挑战吗?

今天我要和大家分享一个关于Yi系列模型的新动态。 你们还记得那个由01.AI从零开始训练的开源大型语言模型吗&#xff1f; Yi系列模型在庞大的3T多语言语料库上训练而成&#xff0c;实力不容小觑。 最近有个好消息&#xff0c;Yi-9B模型已经开源啦&#xff01; 这是Yi系列中…

猫咪挑食是因为什么?解决猫咪挑食效果好的主食冻干推荐

在如今&#xff0c;养猫人士几乎都将自己的小猫咪视作珍宝&#xff0c;宠溺有加。但这样的宠爱有时会导致猫咪出现挑食的问题。那么&#xff0c;猫咪挑食是因为什么&#xff1f;我们该如何应对这种情况呢&#xff1f; 今天为大家分析猫咪挑食是因为什么&#xff0c;再为大家分…

SqlServer 默认值约束示例

创建表&#xff0c;创建时指定 money 字段默认值为0.00&#xff1b; create table t_24 ( account varchar(19) not null, id_card char(18) not null, name varchar(20) not null, money decimal(16,2) default 0.00 not null ); 录入2条记录&#xff0c;money字…

[力扣 Hot100]Day45 二叉树的右视图

题目描述 给定一个二叉树的 根节点 root&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。 出处 思路 层序遍历&#xff0c;选择每层的最后一个。 代码 class Solution { public:vector<int> rightSi…

[机器视觉]halcon十二 条码识别、字符识别之字符识别

[机器视觉]halcon十二 条码识别、字符识别之字符识别 流程 获取图像-》创建模型-》查找文本-》清除模型 效果 算子 create_text_model_reader &#xff1a; 创建文本模型 find_text : 查找文本 get_text_result &#xff1a;获取文本内容 set_text_model_param : 设置文本模板…

STM32CubeIDE基础学习-STM32CubeIDE软件配置下载器方法

STM32CubeIDE基础学习-STM32CubeIDE软件配置下载器方法 文章目录 STM32CubeIDE基础学习-STM32CubeIDE软件配置下载器方法前言第1章 配置ST-LINK下载器第2章 配置DAP下载器总结 前言 这个软件编译完之后&#xff0c;可以使用下载器进行在线下载程序或仿真调试程序&#xff0c;也…