likes
comments
collection
share

揭秘React的执行机制:一文掌握并发模式与中断渲染

作者站长头像
站长
· 阅读数 0

前置概念

Fiber Node: React 的每个组件都对应一个 Fiber 节点,它包含组件的类型、对应的 DOM 节点、子 Fiber、兄弟 Fiber、副作用、状态等信息。

Reconciliation: 协调过程是 React 用来比较新旧虚拟 DOM 树差异的过程,以确定哪些部分需要更新。

Work Loop: Fiber 架构引入了 work loop,它将渲染工作分解为小块任务,这些任务可以被暂停、中断和重新启动,这就是所谓的“时间分片”。

Scheduler: 调度器是 Fiber 架构的核心,它决定哪些任务现在应该执行,哪些可以延迟。

宏观理解

首先从宏观上理解react 的工作流程,主要分为首次渲染和更新两个部分,下面会结合部分源码,源码部分庞大复杂,这里为方便理解整体流程,仅给出核心部分源码,仅作参考。当然你也完全可以忽视代码部分,并不会影响你理解核心概念。

初始渲染(Initial Render)

1. 创建React根节点

当你调用ReactDOM.createRoot(container)时,React会创建一个根节点。这个根节点是整个应用的起点。

const root = ReactDOM.createRoot(document.getElementById('root'));

// 源码参考:packages/react-dom/src/client/ReactDOMRoot.js
export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions,
): RootType {
  // ...省略代码...
  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
  );
  markContainerAsRoot(root.current, container);
  return new ReactDOMRoot(root);
}

2. 开始渲染任务

调用root.render(<App />)时,React开始渲染任务。这个过程是通过一系列内部机制完成的,包括创建更新、调度任务、构建Fiber树、协调和最终提交更改到DOM。

// 开始渲染任务
root.render(<App />);

// 源码参考:packages/react-dom/src/client/ReactDOMRoot.js
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function(
  children: ReactNodeList,
): void {
  const root = this._internalRoot;
   // ...省略代码...
  updateContainer(children, root, null, null);
};

3. 创建更新

创建更新任务,开始调度更新

// 源码参考:/react-reconciler/src/ReactFiberReconciler.new.js
function updateContainer(element, container, parentComponent, callback) {
  const update = createUpdate(eventTime, lane);
  // ...省略代码...
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    // ...省略代码...
    update.callback = callback;
  }
  const root = enqueueUpdate(current, update, lane);
  // ...省略代码...
  scheduleUpdateOnFiber(fiber, lane, eventTime);
  // ...省略代码...
}

4. 调度更新

scheduleUpdateOnFiber函数主要作用就是标记更新和调度更新。

ensureRootIsScheduled函数就是调度更新的具体实现。调度的主要目的是保证应用的响应性和性能,通过智能地调度工作,确保高优先级的更新能够快速完成,同时允许低优先级的工作在必要时被推迟。

scheduleCallback就是调用Scheduler的能力进行调度任务,调度的任务可以分为同步任务和异步任务两大类,对应performConcurrentWorkOnRootperformSyncWorkOnRoot

// 源码参考:/react-reconciler/src/ReactFiberWorkLoop.new.js
function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  // ...省略代码...
  markRootUpdated(root, lane, eventTime);
  // ...省略代码...
  ensureRootIsScheduled(root, eventTime);
  // ...省略代码...
}

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  // ...省略代码...
  if (nextLanes === NoLanes) {
    // Special case: There's nothing to work on.
    return;
  }
  // ...省略代码...
  // Check if there's an existing task. We may be able to reuse it.
  if (existingCallbackPriority === newCallbackPriority) {
    return;
  }
  if (existingCallbackNode != null) {
    // Cancel the existing callback. We'll schedule a new one below.
    cancelCallback(existingCallbackNode);
  }

  let newCallbackNode;
  if (newCallbackPriority === SyncLane) {
    // ...省略代码...
    if (root.tag === LegacyRoot) {
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
    if (supportsMicrotasks) {
      // ...省略代码...
      scheduleMicrotask(...);
    } else {
      scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
    }
    newCallbackNode = null;
  } else {
    // ...省略代码...
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
  // ...省略代码...
}

5. 协调(Reconciliation)

可以看到执行任务的过程,无论是并发模式还是同步模式,都会执行一个workLoopworkLoop再去调用performUnitOfWork。而两者的区别就在于,并发使用了!shouldYield()进行调度控制,这也是Scheduler提供的能力。

performUnitOfWork首先调用beginWork来开始处理工作单元,如果beginWork返回null,则意味着这个工作单元已经完成了,它会调用completeUnitOfWork。如果beginWork返回了另一个工作单元,则将其设置为下一个要处理的工作单元。其本质就是树的一个深度优先遍历。

beginWorkcompleteUnitOfWork 是 React Fiber 协调过程中的两个主要阶段。通过递归处理,收集需要更新的 Fiber 到 effectList,这个列表会在后续的提交阶段commitRoot用来实际应用这些副作用到 DOM 上。

// 源码参考:/react-reconciler/src/ReactFiberWorkLoop.new.js
// 并发(异步)任务
function performConcurrentWorkOnRoot(root, didTimeout) {
  // ...省略代码...
  // 是否时间切片?
  const shouldTimeSlice =
    !includesBlockingLane(root, lanes) &&
    !includesExpiredLane(root, lanes) &&
    (disableSchedulerTimeoutInWorkLoop || !didTimeout);
  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);
  if (exitStatus !== RootInProgress) {
      // ...省略代码...
      root.finishedWork = finishedWork;
      root.finishedLanes = lanes;
      finishConcurrentRender(root, exitStatus, lanes);
    }
  }
}
// 同步任务
function performSyncWorkOnRoot(root) {
  // ...省略代码...
  flushPassiveEffects();
  // ...省略代码...
  let exitStatus = renderRootSync(root, lanes);
  // ...省略代码...
  commitRoot(
    root,
    workInProgressRootRecoverableErrors,
    workInProgressTransitions,
  );
  // ...省略代码...
}

function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
  // ...省略代码...
  do {
    try {
      workLoopConcurrent();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
   // ...省略代码...
}

function renderRootSync(root: FiberRoot, lanes: Lanes) {
  // ...省略代码...
  do {
    try {
      workLoopSync();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
  // ...省略代码...
  return workInProgressRootExitStatus;
}

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

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

function performUnitOfWork(unitOfWork: Fiber): void {
  // ...省略代码...
  next = beginWork(current, unitOfWork, subtreeRenderLanes);
  // ...省略代码...
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

6. 执行提交

提交阶段分为多个子阶段,包括“变更前”阶段、“变更”阶段和“布局”阶段。

在“变更前”阶段,会调用如 getSnapshotBeforeUpdate 这样的生命周期方法。

提交“变更”阶段负责将渲染阶段计算出的更改应用到 DOM 或宿主环境中。

“布局”阶段是在 DOM 变更后读取它的阶段,通常用于执行布局操作,也是类组件的生命周期方法在此触发的原因。

// 源码参考:/react-reconciler/src/ReactFiberWorkLoop.new.js
function commitRootImpl(root) {
  // ...省略代码...
  commitBeforeMutationEffects(root, finishedWork);
  commitMutationEffects(root, renderPriorityLevel, finishedWork);
  commitLayoutEffects(finishedWork, root, lanes);
  // ...省略代码...
}

更新渲染(Updates)

1. 触发更新

更新可以通过setState、hooks或新的props传入等方式来触发。

// 源码参考:/react-reconciler/src/ReactFiberClassComponent.new.js
// 源码参考:/react-reconciler/src/ReactFiberHooks.new.js
function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  // ...省略代码...
  if (root !== null) {
    // 可以理解为触发更新的核心就是触发 scheduleUpdateOnFiber
    scheduleUpdateOnFiber(root, fiber, lane, eventTime);
  }
  // ...省略代码...
}

触发更新之后,更新的机制最终还是调用 scheduleUpdateOnFiber

流程概览图

揭秘React的执行机制:一文掌握并发模式与中断渲染

核心机制

如何实现中断

React 的中断和恢复机制是基于其 Fiber 架构实现的。Fiber 架构是 React 16 中引入的一种新的内部机制,它允许 React 工作在单个任务上,如果需要,可以将控制权交回给浏览器,然后再继续从中断点恢复工作。这种能力是通过将工作分解成许多小的任务单元来实现的,每个任务单元完成后,React 都可以决定是继续处理下一个任务单元,还是中断以处理更紧急的事情。

1、work Loop

React 的渲染引擎有一个循环,称为“work loop”,在这个循环中,React 会遍历 Fiber 树来执行渲染工作。这个循环是可以被中断的,这就是所谓的“时间分片”。

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

在这个函数中,workInProgress 是当前正在处理的 Fiber 节点,performUnitOfWork 是处理单个 Fiber 节点的函数,shouldYield 是一个检查是否应该中断工作循环的函数,它可能会基于浏览器的帧率或者剩余时间来决定。

2、Lane 模型

Lane模型,简单理解就是一种用来标记跟踪不同优先级的更新。每个更新被赋予一个或多个lanes,这些lanes代表了更新的优先级和类型。React调度器使用这些lanes来决定哪些更新应该被优先处理。通过这种机制,React能够更智能地处理多重更新,优先执行更重要的更新。

3、performUnitOfWork

这个函数负责处理单个 Fiber 节点的工作,包括调用生命周期方法、钩子函数,以及创建和更新 DOM 节点。

performUnitOfWork会进行“深度优先遍历”,首先beginWork 开始处理一个 Fiber 节点,如果这个节点有子节点,它会返回第一个子节点,如果没有则调用completeUnitOfWorkcompleteUnitOfWork会完成一个 Fiber 节点的工作和收集副作用(side effects),如果当前节点有兄弟节点,那么下一个工作单元将是兄弟节点;如果没有将回溯到父节点,继续完成其余的工作。.

function performUnitOfWork(unitOfWork: Fiber): void {
  // ...省略代码...
  next = beginWork(current, unitOfWork, subtreeRenderLanes);
  // ...省略代码...
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

4、shouldYield

shouldYield是调用的Scheduler的能力,用于判断当前 React 任务是否应该暂停以让浏览器处理其他任务的函数。它主要基于时间消耗和浏览器事件的检测来做出决策。

  1. 帧率和剩余时间:通过计算自上次任务开始以来已经消耗的时间(timeElapsed)来决定是否应该让出控制权。这个时间与浏览器的帧时间(5ms)进行比较,以确定是否已经占用了足够的时间。
  2. 高优先级任务的处理:检查是否存在高优先级任务,如用户输入或者画面更新。如果检测到这些任务,React 将暂停当前工作以响应这些任务。

这样的设计是为了确保用户界面的流畅性和响应性,同时允许长时间运行的任务在不影响用户体验的情况下执行。

function shouldYield() {
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    // The main thread has only been blocked for a really short amount of time;
    // smaller than a single frame. Don't yield yet.
    return false;
  }
  if (enableIsInputPending) {
     // ...省略代码
  }
}

5、Scheduler

React 的调度器是负责任务调度的部分。调度器通过unstable_scheduleCallback 统一调度任务,根据任务的优先级赋予不同的到期时间,并且通过两个队列去维护同步任务和延时任务。

执行任务是调用的requestHostCallback,在 DOM 和 Worder 环境下,react 使用 MessageChannel 来管理任务执行,其内部也有一个 work loop,通过从任务队列中取出任务,并根据任务的过期时间来判断是否执行任务的回调,没错,这里也会用到shouldYield的能力。

function workLoop(hasTimeRemaining, initialTime) {
  // ...省略代码
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      break;
    }
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      // ...省略代码
      const continuationCallback = callback(didUserCallbackTimeout);
      // ...省略代码
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  // ...省略代码
}

总结

React 的中断机制允许它在执行更新时能够根据任务的优先级、剩余时间策略来智能地中断和恢复工作。这与Fiber 节点的构造、工作循环、优先级模型、以及调度器的协作完成的。Fiber 架构的引入让 React 有了足够的灵活性来实现这种并发处理机制,从而提高应用的响应性和性能。

如何实现恢复

在 React 的 Fiber 架构中,每个工作单元(即 Fiber)在执行过程中都可以被中断,并且可以在稍后的时间恢复执行。React 实现这一机制的关键在于它如何追踪工作的进度,并且能够在适当的时候将控制权交回给浏览器,然后在浏览器空闲时恢复工作。下面是一些关键的概念和源码片段,帮助理解 React 是如何恢复中断的工作的:

1、Work In Progress (WIP) Tree

React 会维护一个正在进行中的工作树(WIP Tree),它是当前渲染阶段的 Fiber 树的副本。当工作开始时,React 会创建这个 WIP Tree,并在工作过程中更新它。

2、Fiber 结构

每个 Fiber 节点都包含了足够的信息,使得 React 可以在任何时候中断和恢复工作。这些信息包括:

  • child: 指向第一个子节点
  • sibling: 指向下一个兄弟节点
  • return: 指向父节点
  • memoizedState: 记录组件的状态
  • updateQueue: 记录组件的更新

3、中断时的保存状态

当 React 决定中断当前的工作时(前面说过的shouldYield 函数触发),它不需要做特别的保存操作,因为所有的工作状态都已经存储在 WIP Fiber 树中了。当前的工作单元(当前执行的 Fiber 节点)和它的进度可以从正在处理的 Fiber 节点中直接获得。

4、恢复工作

当 React 准备恢复工作时,它会从上次中断的地方开始。这是通过重新开始 work loop 实现的,并且 work loop 会从上次中断的 workInProgress Fiber 节点开始执行。

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

在上述循环中,如果 workInProgress 不为 null,则意味着有工作需要继续进行。React 会调用 performUnitOfWork 来继续处理下一个工作单元。也就回到了performUnitOfWorkcompleteUnitOfWork

总结

React 的恢复机制依赖于每个 Fiber 节点的结构和 work loop 的设计。每个 Fiber 节点都保存了足够的上下文信息,使得 React 可以在任何时候安全地中断和恢复工作。

如何理解 React 的调度器(scheduler)和 Fiber 架构中的循环(loop)

Scheduler 的循环(Scheduler Loop)

React 的调度器是负责管理任务的优先级和执行的部分。在 React 16+ 中,调度器利用了浏览器的 requestIdleCallback(或者其自身的 polyfill)来实现一个循环,这个循环会在主线程空闲时执行低优先级的工作。

  • 循环的目的:调度器的循环主要是为了分配和管理任务的执行。它会根据任务的优先级和过期时间来决定何时执行哪个任务。
  • 工作方式:当 React 需要执行更新时,它会将更新任务注册到调度器中。调度器会根据当前的执行上下文(如浏览器是否空闲)和任务的优先级来决定执行的顺序和时间。
  • 时间切片:调度器支持时间切片(time slicing),这意味着它可以将长任务切分成多个小任务,以避免阻塞主线程,从而提高应用的响应性。

Fiber 架构的循环(Fiber Loop)

Fiber 架构是 React 16 中引入的一种新的协调算法。它允许 React 在渲染过程中暂停、中断、恢复和重用工作。Fiber 架构中的“循环”并不是传统意义上的事件循环,而是 React 内部的一种递归遍历机制,是一个可以被中断和恢复的工作流程。

  • 循环的目的:Fiber 架构的循环是为了在组件树上高效地执行协调操作,确定哪些组件需要更新、创建或销毁。
  • 工作方式:Fiber 实际上是对组件树的节点的一个抽象,每个 Fiber 节点代表一个工作单元。React 会遍历这些节点,并根据需要执行相应的渲染工作。
  • 中断和恢复:Fiber 架构的关键特性是能够将工作分割成小块,并在需要处理更高优先级的更新时中断当前工作。一旦高优先级的工作完成,它可以恢复之前的工作。

关联和区别

  • 关联:调度器和 Fiber 架构共同协作以实现平滑的用户界面更新。调度器决定何时执行工作,而 Fiber 架构提供了一种机制来组织和管理这些工作。
  • 区别
    • 层级不同:调度器工作在更高的层级,它管理所有的任务,不仅仅是与渲染相关的。而 Fiber 架构专注于渲染相关的任务。
    • 工作单元不同:调度器的任务可能是任何类型的工作,比如数据获取或动画。Fiber 架构的工作单元是组件的渲染和协调。
    • 目标不同:调度器的目标是优化整个应用的性能,特别是在多任务环境下。Fiber 的目标是优化渲染过程,使其更加平滑和可中断。

总结

总体来说,调度器和 Fiber 架构是互相补充的。调度器安排和优先处理任务,而 Fiber 架构则为这些任务的执行提供了灵活性和中断能力。通过这两个机制的结合,React 能够提供一个既快速又流畅的用户界面构建体验。

最后

本文在讨论React的渲染机制时省略了大量源码的细节,主要目的是为了理解其中的关键概念和核心流程。如果您发现有任何错误或不准确之处,欢迎提出指正,一起交流,非常感谢您的反馈和建议。