图解React图解React
  • 原理解析
  • 高频算法
  • 面试题
⌘ K
基本概念
宏观包结构
两大工作循环
高频对象
运行核心
启动过程
reconciler 运作流程
优先级管理
调度原理
fiber 树构造(基础准备)
fiber 树构造(初次创建)
fiber 树构造(对比更新)
fiber 树渲染
状态管理
状态与副作用
Hook 原理(概览)
Hook 原理(状态Hook)
Hook 原理(副作用Hook)
context 原理
交互
合成事件
Copyright © 2023 | Powered by dumi

fiber 树构造(基础准备)

在 React 运行时中, fiber树构造位于react-reconciler包.

在正式解读fiber树构造之前, 再次回顾一下reconciler 运作流程的 4 个阶段:

  1. 输入阶段: 衔接react-dom包, 承接fiber更新请求(可以参考React 应用的启动过程).
  2. 注册调度任务: 与调度中心(scheduler包)交互, 注册调度任务task, 等待任务回调(可以参考React 调度原理(scheduler)).
  3. 执行任务回调: 在内存中构造出fiber树和DOM对象, 也是fiber 树构造的重点内容.
  4. 输出: 与渲染器(react-dom)交互, 渲染DOM节点.

fiber树构造处于上述第 3 个阶段, 可以通过不同的视角来理解fiber树构造在React运行时中所处的位置:

  • 从scheduler调度中心的角度来看, 它是任务队列taskQueue中的一个具体的任务回调(task.callback).
  • 从React 工作循环的角度来看, 它属于fiber树构造循环.

由于fiber 树构造源码量比较大, 本系列根据React运行的内存状态, 分为 2 种情况来说明:

  1. 初次创建: 在React应用首次启动时, 界面还没有渲染, 此时并不会进入对比过程, 相当于直接构造一棵全新的树.
  2. 对比更新: React应用启动后, 界面已经渲染. 如果再次发生更新, 创建新fiber之前需要和旧fiber进行对比. 最后构造的 fiber 树有可能是全新的, 也可能是部分更新的.

无论是初次创建还是对比更新, 基础概念都是通用的, 本节将介绍这些基础知识, 为正式进入fiber树构造做准备.

ReactElement, Fiber, DOM 三者的关系

在React 应用中的高频对象一文中, 已经介绍了ReactElement和Fiber对象的数据结构. 这里我们梳理出ReactElement, Fiber, DOM这 3 种对象的关系

  1. ReactElement 对象(type 定义在shared 包中)

    • 所有采用jsx语法书写的节点, 都会被编译器转换, 最终会以React.createElement(...)的方式, 创建出来一个与之对应的ReactElement对象
  2. fiber 对象(type 类型的定义在ReactInternalTypes.js中)

    • fiber对象是通过ReactElement对象进行创建的, 多个fiber对象构成了一棵fiber树, fiber树是构造DOM树的数据模型, fiber树的任何改动, 最后都体现到DOM树.
  3. DOM 对象: 文档对象模型

    • DOM将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合, 也就是常说的DOM树.
    • JavaScript可以访问和操作存储在 DOM 中的内容, 也就是操作DOM对象, 进而触发 UI 渲染.

它们之间的关系反映了我们书写的 JSX 代码到 DOM 节点的转换过程:

注意:

  • 开发人员能够控制的是JSX, 也就是ReactElement对象.
  • fiber树是通过ReactElement生成的, 如果脱离了ReactElement,fiber树也无从谈起. 所以是ReactElement树(不是严格的树结构, 为了方便也称为树)驱动fiber树.
  • fiber树是DOM树的数据模型, fiber树驱动DOM树

开发人员通过编程只能控制ReactElement树的结构, ReactElement树驱动fiber树, fiber树再驱动DOM树, 最后展现到页面上. 所以fiber树的构造过程, 实际上就是ReactElement对象到fiber对象的转换过程.

全局变量

从React 工作循环的角度来看, 整个构造过程被包裹在fiber树构造循环中(对应源码位于ReactFiberWorkLoop.js).

在React运行时, ReactFiberWorkLoop.js闭包中的全局变量会随着fiber树构造循环的进行而变化, 现在查看其中重要的全局变量(源码链接):

// 当前React的执行栈(执行上下文)
let executionContext: ExecutionContext = NoContext;
// 当前root节点
let workInProgressRoot: FiberRoot | null = null;
// 正在处理中的fiber节点
let workInProgress: Fiber | null = null;
// 正在渲染的车道(复数)
let workInProgressRootRenderLanes: Lanes = NoLanes;
// 包含所有子节点的优先级, 是workInProgressRootRenderLanes的超集
// 大多数情况下: 在工作循环整体层面会使用workInProgressRootRenderLanes, 在begin/complete阶段层面会使用 subtreeRenderLanes
let subtreeRenderLanes: Lanes = NoLanes;
// 一个栈结构: 专门存储当前节点的 subtreeRenderLanes
const subtreeRenderLanesCursor: StackCursor<Lanes> = createCursor(NoLanes);
// fiber构造完后, root节点的状态: completed, errored, suspended等
let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
// 重大错误
let workInProgressRootFatalError: mixed = null;
// 整个render期间所使用到的所有lanes
let workInProgressRootIncludedLanes: Lanes = NoLanes;
// 在render期间被跳过(由于优先级不够)的lanes: 只包括未处理的updates, 不包括被复用的fiber节点
let workInProgressRootSkippedLanes: Lanes = NoLanes;
// 在render期间被修改过的lanes
let workInProgressRootUpdatedLanes: Lanes = NoLanes;
// 防止无限循环和嵌套更新
const NESTED_UPDATE_LIMIT = 50;
let nestedUpdateCount: number = 0;
let rootWithNestedUpdates: FiberRoot | null = null;
const NESTED_PASSIVE_UPDATE_LIMIT = 50;
let nestedPassiveUpdateCount: number = 0;
// 发起更新的时间
let currentEventTime: number = NoTimestamp;
let currentEventWipLanes: Lanes = NoLanes;
let currentEventPendingLanes: Lanes = NoLanes;

在源码中, 大部分变量都带有英文注释(读者可自行查阅), 此处只列举了fiber树构造循环中最核心的变量

执行上下文

在全局变量中有executionContext, 代表渲染期间的执行栈(或叫做执行上下文), 它也是一个二进制表示的变量, 通过位运算进行操作(参考React 算法之位运算). 在源码中一共定义了 8 种执行栈:

type ExecutionContext = number;
export const NoContext = /* */ 0b0000000;
const BatchedContext = /* */ 0b0000001;
const EventContext = /* */ 0b0000010;
const DiscreteEventContext = /* */ 0b0000100;
const LegacyUnbatchedContext = /* */ 0b0001000;
const RenderContext = /* */ 0b0010000;
const CommitContext = /* */ 0b0100000;

上文回顾了reconciler 运作流程的 4 个阶段, 这 4 个阶段只是一个整体划分. 如果具体到每一次更新, 是有差异的. 比如说: Legacy模式下的首次更新, 不会经过调度中心(第 2 阶段),而是直接进入fiber树构造(第 3 阶段).

事实上正是executionContext在操控reconciler 运作流程(源码体现在scheduleUpdateOnFiber 函数).

export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
if (lane === SyncLane) {
// legacy或blocking模式
if (
(executionContext & LegacyUnbatchedContext) !== NoContext &&
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
performSyncWorkOnRoot(root);
} else {
// 后续的更新
// 进入第2阶段, 注册调度任务
ensureRootIsScheduled(root, eventTime);
if (executionContext === NoContext) {
// 如果执行上下文为空, 会取消调度任务, 手动执行回调
// 进入第3阶段, 进行fiber树构造
flushSyncCallbackQueue();
}
}
} else {
// concurrent模式
// 无论是否初次更新, 都正常进入第2阶段, 注册调度任务
ensureRootIsScheduled(root, eventTime);
}
}

在 render 过程中, 每一个阶段都会改变executionContext(render 之前, 会设置executionContext |= RenderContext; commit 之前, 会设置executionContext |= CommitContext), 假设在render过程中再次发起更新(如在UNSAFE_componentWillReceiveProps生命周期中调用setState)则可通过executionContext来判断当前的render状态.

双缓冲技术(double buffering)

在全局变量中有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树.

此处涉及到 2 个全局对象fiberRoot和HostRootFiber, 在React 应用的启动过程中有详细的说明.

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

  1. 构造过程中, fiberRoot.current指向当前界面对应的fiber树.

  1. 构造完成并渲染, 切换fiberRoot.current指针, 使其继续指向当前界面对应的fiber树(原来代表界面的 fiber 树, 变成了内存中).

优先级 {#lanes}

在全局变量中有不少变量都以 Lanes 命名(如workInProgressRootRenderLanes,subtreeRenderLanes其作用见上文注释), 它们都与优先级相关.

在前文React 中的优先级管理中, 我们介绍了React中有 3 套优先级体系, 并了解了它们之间的关联. 现在fiber树构造过程中, 将要深入分析车道模型Lane的具体应用.

在整个react-reconciler包中, Lane的应用可以分为 3 个方面:

update优先级(update.lane) {#update-lane}

在React 应用中的高频对象一文中, 介绍过update对象, 它是一个环形链表. 对于单个update对象来讲, update.lane代表它的优先级, 称之为update优先级.

观察其构造函数(源码链接),其优先级是由外界传入.

export function createUpdate(eventTime: number, lane: Lane): Update<*> {
const update: Update<*> = {
eventTime,
lane,
tag: UpdateState,
payload: null,
callback: null,
next: null,
};
return update;
}

在React体系中, 有 2 种情况会创建update对象:

  1. 应用初始化: 在react-reconciler包中的updateContainer函数中(源码)

    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;
    }
  2. 发起组件更新: 假设在 class 组件中调用setState(源码)

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);
},
};

可以看到, 无论是应用初始化或者发起组件更新, 创建update.lane的逻辑都是一样的, 都是根据当前时间, 创建一个 update 优先级.

requestUpdateLane:

export function requestUpdateLane(fiber: Fiber): Lane {
// Special cases
const mode = fiber.mode;
if ((mode & BlockingMode) === NoMode) {
// legacy 模式
return (SyncLane: Lane);
} else if ((mode & ConcurrentMode) === NoMode) {
// blocking模式
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
? (SyncLane: Lane)
: (SyncBatchedLane: Lane);
}
// concurrent模式
if (currentEventWipLanes === NoLanes) {
currentEventWipLanes = workInProgressRootIncludedLanes;
}
const isTransition = requestCurrentTransition() !== NoTransition;
if (isTransition) {
// 特殊情况, 处于suspense过程中
if (currentEventPendingLanes !== NoLanes) {
currentEventPendingLanes =
mostRecentlyUpdatedRoot !== null
? mostRecentlyUpdatedRoot.pendingLanes
: NoLanes;
}
return findTransitionLane(currentEventWipLanes, currentEventPendingLanes);
}
// 正常情况, 获取调度优先级
const schedulerPriority = getCurrentPriorityLevel();
let lane;
if (
(executionContext & DiscreteEventContext) !== NoContext &&
schedulerPriority === UserBlockingSchedulerPriority
) {
// executionContext 存在输入事件. 且调度优先级是用户阻塞性质
lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes);
} else {
// 调度优先级转换为车道模型
const schedulerLanePriority =
schedulerPriorityToLanePriority(schedulerPriority);
lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
}
return lane;
}

可以看到requestUpdateLane的作用是返回一个合适的 update 优先级.

  1. legacy 模式: 返回SyncLane
  2. blocking 模式: 返回SyncLane
  3. concurrent 模式:
    • 正常情况下, 根据当前的调度优先级来生成一个lane.
    • 特殊情况下(处于 suspense 过程中), 会优先选择TransitionLanes通道中的空闲通道(如果所有TransitionLanes通道都被占用, 就取最高优先级. 源码).

最后通过scheduleUpdateOnFiber(current, lane, eventTime);函数, 把update.lane正式带入到了输入阶段.

scheduleUpdateOnFiber是输入阶段的必经函数, 在本系列的文章中已经多次提到, 此处以update.lane的视角分析:

export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
if (lane === SyncLane) {
// legacy或blocking模式
if (
(executionContext & LegacyUnbatchedContext) !== NoContext &&
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime); // 注册回调任务
if (executionContext === NoContext) {
flushSyncCallbackQueue(); // 取消schedule调度 ,主动刷新回调队列,
}
}
} else {
// concurrent模式
ensureRootIsScheduled(root, eventTime);
}
}

当lane === SyncLane也就是 legacy 或 blocking 模式中, 注册完回调任务之后(ensureRootIsScheduled(root, eventTime)), 如果执行上下文为空, 会取消 schedule 调度, 主动刷新回调队列flushSyncCallbackQueue().

这里包含了一个热点问题(setState到底是同步还是异步)的标准答案:

  • 如果逻辑进入flushSyncCallbackQueue(executionContext === NoContext), 则会主动取消调度, 并刷新回调, 立即进入fiber树构造过程. 当执行setState下一行代码时, fiber树已经重新渲染了, 故setState体现为同步.
  • 正常情况下, 不会取消schedule调度. 由于schedule调度是通过MessageChannel触发(宏任务), 故体现为异步.

渲染优先级(renderLanes)

这是一个全局概念, 每一次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);
}

可以看到, 无论是Legacy还是Concurrent模式, 在正式render之前, 都会调用getNextLanes获取一个优先级(源码链接).

// ...省略部分代码
export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
// 1. check是否有等待中的lanes
const 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是否有已过期的lanes
if (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;
}

getNextLanes会根据fiberRoot对象上的属性(expiredLanes, suspendedLanes, pingedLanes等), 确定出当前最紧急的lanes.

此处返回的lanes会作为全局渲染的优先级, 用于fiber树构造过程中. 针对fiber对象或update对象, 只要它们的优先级(如: fiber.lanes和update.lane)比渲染优先级低, 都将会被忽略.

fiber优先级(fiber.lanes)

在React 应用中的高频对象一文中, 介绍过fiber对象的数据结构. 其中有 2 个属性与优先级相关:

  1. fiber.lanes: 代表本节点的优先级
  2. fiber.childLanes: 代表子节点的优先级
    从FiberNode的构造函数中可以看出, fiber.lanes和fiber.childLanes的初始值都为NoLanes, 在fiber树构造过程中, 使用全局的渲染优先级(renderLanes)和fiber.lanes判断fiber节点是否更新(源码地址).
    • 如果全局的渲染优先级renderLanes不包括fiber.lanes, 证明该fiber节点没有更新, 可以复用.
    • 如果不能复用, 进入创建阶段.
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() ||
// Force a re-render if the implementation changed due to hot reload:
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
// 本`fiber`节点的没有更新, 可以复用, 进入bailout逻辑
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
// 不能复用, 创建新的fiber节点
workInProgress.lanes = NoLanes; // 重置优先级为 NoLanes
switch (workInProgress.tag) {
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,
// 正常情况下渲染优先级会被用于fiber树的构造过程
renderLanes,
);
}
}
}

栈帧管理

在React源码中, 每一次执行fiber树构造(也就是调用performSyncWorkOnRoot或者performConcurrentWorkOnRoot函数)的过程, 都需要一些全局变量来保存状态. 在上文中已经介绍最核心的全局变量.

如果从单个变量来看, 它们就是一个个的全局变量. 如果将这些全局变量组合起来, 它们代表了当前fiber树构造的活动记录. 通过这一组全局变量, 可以还原fiber树构造过程(比如时间切片的实现过程(参考React 调度原理), fiber树构造过程被打断之后需要还原进度, 全靠这一组全局变量). 所以每次fiber树构造是一个独立的过程, 需要独立的一组全局变量, 在React内部把这一个独立的过程封装为一个栈帧stack(简单来说就是每次构造都需要独立的空间. 对于栈帧的深入理解, 请读者自行参考其他资料).

所以在进行fiber树构造之前, 如果不需要恢复上一次构造进度, 都会刷新栈帧(源码在prepareFreshStack 函数)

function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
// 如果fiberRoot变动, 或者update.lane变动, 都会刷新栈帧, 丢弃上一次渲染进度
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
resetRenderTimer();
// 刷新栈帧
prepareFreshStack(root, lanes);
startWorkOnPendingInteractions(root, lanes);
}
}
/**
刷新栈帧: 重置 FiberRoot上的全局属性 和 `fiber树构造`循环过程中的全局变量
*/
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, 并将其设置成全局 workInProgress
workInProgressRootRenderLanes =
subtreeRenderLanes =
workInProgressRootIncludedLanes =
lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
}

注意其中的createWorkInProgress(root.current, null), 其参数root.current即HostRootFiber, 作用是给HostRootFiber创建一个alternate副本.workInProgress指针指向这个副本(即workInProgress = HostRootFiber.alternate), 在上文double buffering中分析过, HostRootFiber.alternate是正在构造的fiber树的根节点.

总结

本节是fiber树构造的准备篇, 首先在宏观上从不同的视角(任务调度循环, fiber树构造循环)介绍了fiber树构造在React体系中所处的位置, 然后深入react-reconciler包分析fiber树构造过程中需要使用到的全局变量, 并解读了双缓冲技术和优先级(车道模型)的使用, 最后解释栈帧管理的实现细节. 有了这些基础知识, fiber树构造的具体实现过程会更加简单清晰.