Skip to content

React 18 渲染流程 ​

以 React 18 源码为线索,梳理一次更新从 createRoot().render() 到 DOM 提交的完整链路:渲染入口 → 优先级体系 → 调度流程 → 同步/并发渲染 → Fiber 执行单元 → commit 阶段。代码摘自 packages/react-dom、packages/react-reconciler 与 packages/scheduler,省略号处为与主线无关的实现。

目录 ​

  1. 渲染入口
  2. React 优先级
  3. 调度流程
  4. 同步调度
  5. 并发调度
  6. 执行单元
  7. commit 阶段

一、渲染入口 ​

React 17 通过 ReactDOM.render 渲染项目,React 18 改为用 createRoot 创建应用入口:

tsx
const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement,
);

root.render(<App />);

1. createRoot ​

packages/react-dom/src/client/ReactDOMRoot.js

js
export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions,
): RootType {
  // ...

  // 创建根节点
  const root = createContainer(
    container,
    ConcurrentRoot, // 新版本的根节点,值为 1
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
  );

  const rootContainerElement: Document | Element | DocumentFragment =
    container.nodeType === COMMENT_NODE
      ? (container.parentNode: any)
      : container;

  // 绑定 React 合成事件
  listenToAllSupportedEvents(rootContainerElement);

  // 返回根节点,其中会通过原型链增加 render 等函数
  return new ReactDOMRoot(root);
}

2. ReactDOMRoot ​

packages/react-dom/src/client/ReactDOMRoot.js

js
function ReactDOMRoot(internalRoot: FiberRoot) {
  this._internalRoot = internalRoot;
}

// 使用 createRoot 创建根节点后,会使用 render 渲染根组件
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function (
  children: ReactNodeList,
): void {
  const root = this._internalRoot;
  if (root === null) {
    throw new Error('Cannot update an unmounted root.');
  }

  // 是 React 应用渲染和更新流程的入口点
  updateContainer(children, root, null, null);
};

3. updateContainer ​

packages/react-reconciler/src/ReactFiberReconciler.old.js

js
export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  // ...

  // 将更新加入队列
  const root = enqueueUpdate(current, update, lane);
  if (root !== null) {
    // 触发调度
    scheduleUpdateOnFiber(root, current, lane, eventTime);
    // 关联过渡更新
    entangleTransitions(root, current, lane);
  }

  return lane;
}

4. scheduleUpdateOnFiber ​

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

调度的入口点,内部调用 ensureRootIsScheduled:

js
export function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  // 检查是否存在嵌套更新,存在则报错提示
  checkForNestedUpdates();

  // 标记根节点有一个待处理的更新,记录更新的车道和时间戳
  markRootUpdated(root, lane, eventTime);

  // 检查是否在渲染过程中触发更新,常因为错误写法
  if (
    (executionContext & RenderContext) !== NoLanes &&
    root === workInProgressRoot
  ) {
    // 这些更新会被特殊处理,不会立即执行,而是被记录下来
    workInProgressRootRenderPhaseUpdatedLanes = mergeLanes(
      workInProgressRootRenderPhaseUpdatedLanes,
      lane,
    );
  } else {
    // 核心调度函数
    ensureRootIsScheduled(root, eventTime);
  }
}

二、React 优先级 ​

在介绍 ensureRootIsScheduled 之前,需要先了解 React 的三套优先级概念:调度任务的优先级、车道的优先级、事件优先级。

1. 调度任务的优先级 ​

packages/scheduler/src/SchedulerPriorities.js

Scheduler 调度任务的优先级分为 5 种:

js
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
  • ImmediatePriority:离散事件,如 click、keydown、input。
  • UserBlockingPriority:连续事件,如 scroll、drag、mouseover。
  • 其余是默认优先级 NormalPriority、低优先级 LowPriority、空闲优先级 IdlePriority。

2. 车道的优先级 ​

packages/react-reconciler/src/ReactFiberLane.old.js

React 有自己的一套优先级机制,共有 31 种车道优先级。

React 18 的 31 种车道优先级

3. 事件优先级 ​

packages/react-reconciler/src/ReactEventPriorities.old.js

共有四种事件优先级:DiscreteEventPriority 离散事件优先级、ContinuousEventPriority 连续事件优先级、DefaultEventPriority 默认事件优先级、IdleEventPriority 空闲时间优先级:

js
export const DiscreteEventPriority: EventPriority = SyncLane;
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
export const DefaultEventPriority: EventPriority = DefaultLane;
export const IdleEventPriority: EventPriority = IdleLane;

DiscreteEventPriority 离散事件优先级的值为 SyncLane,即同步车道。

packages/react-dom/src/events/ReactDOMEventListener.js

在 getEventPriority 函数中,会根据不同的事件分配优先级:离散事件 click 通过 switch 语句对应到 DiscreteEventPriority,即同步车道;对于 message 类型,则根据 Scheduler 调度任务的优先级匹配对应的事件优先级,即对应到车道优先级:

js
case 'message': {
  // 可能处于 Scheduler 回调中
  const schedulerPriority = getCurrentSchedulerPriorityLevel();
  switch (schedulerPriority) {
    case ImmediateSchedulerPriority:
      return DiscreteEventPriority;
    case UserBlockingSchedulerPriority:
      return ContinuousEventPriority;
    case NormalSchedulerPriority:
    case LowSchedulerPriority:
      // 低优先级暂按默认事件优先级处理,可能与 hydration 使用同一条车道
      return DefaultEventPriority;
    case IdleSchedulerPriority:
      return IdleEventPriority;
    default:
      return DefaultEventPriority;
  }
}

事件优先级与调度优先级可以通过车道相互转换。

三、调度流程 ​

1. ensureRootIsScheduled ​

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

ensureRootIsScheduled 决定了这次是同步调度还是并发调度。

什么时候是同步车道 SyncLane?

  1. 离散事件。
  2. 非并发模式,即用 ReactDOM.render() 创建的 Legacy 模式应用。
  3. 错误边界处理,React 会用 SyncLane 确保错误处理立即执行。
  4. flushSync。

同步任务调用 performSyncWorkOnRoot,并发任务调用 performConcurrentWorkOnRoot:

js
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  const existingCallbackNode = root.callbackNode;

  // 1. 检查是否有车道被饿死,标记为过期
  markStarvedLanesAsExpired(root, currentTime);

  // 2. 确定下一个要工作的车道
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

  if (nextLanes === NoLanes) {
    // 没有需要处理的车道
    if (existingCallbackNode !== null) {
      cancelCallback(existingCallbackNode);
    }
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
  }

  // 3. 返回其中最高优先级的车道或车道组
  const newCallbackPriority = getHighestPriorityLane(nextLanes);

  // 4. 检查是否需要重新调度任务
  const existingCallbackPriority = root.callbackPriority;

  if (
    existingCallbackPriority === newCallbackPriority &&
    // act 的特殊处理:如果当前调度的是 Scheduler 任务而不是 act 任务,
    // 需要取消并重新调度到 act 队列
    !(
      __DEV__ &&
      ReactCurrentActQueue.current !== null &&
      existingCallbackNode !== fakeActCallbackNode
    )
  ) {
    if (__DEV__) {
      // 复用已有任务时它必须存在;离散更新的微任务不可取消,值为 null
      if (
        existingCallbackNode == null &&
        existingCallbackPriority !== SyncLane
      ) {
        console.error(
          'Expected scheduled callback to exist. This error is likely caused by a bug in React. Please file an issue.',
        );
      }
    }
    // 优先级没有变化,复用已有任务直接返回
    return;
  }

  if (existingCallbackNode != null) {
    // 5. 取消现有的任务
    cancelCallback(existingCallbackNode);
  }

  // 调度新的回调
  let newCallbackNode;
  // 6. 同步任务调度,newCallbackPriority 是 nextLanes 中的最高优先级
  if (newCallbackPriority === SyncLane) {
    // 同步回调会被放入一个内部队列
    if (root.tag === LegacyRoot) {
      if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy !== null) {
        ReactCurrentActQueue.didScheduleLegacyUpdate = true;
      }
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }

    // 微任务或立即任务调度
    if (supportsMicrotasks) {
      if (__DEV__ && ReactCurrentActQueue.current !== null) {
        // 在 act 内部使用 act 队列,保证在当前作用域结束时统一刷新
        ReactCurrentActQueue.current.push(flushSyncCallbacks);
      } else {
        scheduleMicrotask(() => {
          // Safari 中追加 iframe 会强制微任务执行,
          // 而 React 不支持在 render 或 commit 中途执行回调,这里要排除这两种情况
          if (
            (executionContext & (RenderContext | CommitContext)) ===
            NoContext
          ) {
            flushSyncCallbacks();
          }
        });
      }
    } else {
      // 用立即任务刷新队列
      scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
    }
    newCallbackNode = null;
  } else {
    // 并发任务调度:先把事件优先级转换为 Scheduler 优先级
    let schedulerPriorityLevel;
    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority: // 离散事件优先级
        schedulerPriorityLevel = ImmediateSchedulerPriority;
        break;
      case ContinuousEventPriority: // 连续事件优先级
        schedulerPriorityLevel = UserBlockingSchedulerPriority;
        break;
      case DefaultEventPriority: // 默认事件优先级
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
      case IdleEventPriority: // 空闲事件优先级
        schedulerPriorityLevel = IdleSchedulerPriority;
        break;
      default:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
    }

    // 调度并发任务
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      // 并发任务的回调函数
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }

  // 7. 更新根节点的回调优先级和节点
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

提示:点击这类离散事件是同步车道,即同步任务,会调用 performSyncWorkOnRoot 处理。SyncLane 同步车道的更新是同步更新,其余都是并发更新。

2. scheduleSyncCallback ​

把回调函数 performSyncWorkOnRoot 添加到数组 syncQueue 中,并在下一个微任务中依次调用 flushSyncCallbacks,或在 flushSync 函数结束时刷新:

js
export function scheduleSyncCallback(callback: SchedulerCallback) {
  // 把回调推入内部队列,要么在下一个 tick 刷新,
  // 要么在 flushSyncCallbackQueue 被调用时更早刷新
  if (syncQueue === null) {
    syncQueue = [callback];
  } else {
    // 直接推入已有队列,无需再次调度:队列创建时已经调度过一次
    syncQueue.push(callback);
  }
}

提示:微任务使用 queueMicrotask,不支持时降级为 Promise.then,再不支持则降级为 setTimeout。除了在微任务中处理同步队列,也可以手动调用 flushSync API:它会先执行传入的回调函数,再调用 flushSyncCallbacks 执行 syncQueue 中的回调,从而强制 React 在回调内同步刷新更新,确保 DOM 立即更新。

3. scheduleCallback ​

scheduleCallback 即 scheduler 包中的 unstable_scheduleCallback。

它负责创建调度任务、设置过期时间:过期任务加入 taskQueue,未过期任务加入 timerQueue;未过期的以开始时间作为排序依据,已过期的以过期时间作为排序依据。浏览器有空闲时间时,Scheduler 调用 flushWork 处理任务队列,flushWork 再调用 workLoop 依次取出 taskQueue 的第一条执行:

js
function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 获取当前时间
  var currentTime = getCurrentTime();
  // 任务开始时间
  var startTime;
  if (typeof options === "object" && options !== null) {
    // 判断用户是否传入了自定义的延迟时间
    var delay = options.delay;
    if (typeof delay === "number" && delay > 0) {
      // 有延迟时间,任务开始时间就是当前时间加上延迟时间
      startTime = currentTime + delay;
    } else {
      // 没有延迟时间,任务需要立刻开始
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }

  // 计算间隔时间
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT; // 10000
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT; // 5000
      break;
  }
  // 过期时间 = 任务开始时间 + timeout
  // 立即执行的优先级(ImmediatePriority)timeout 为 -1,意味着立刻就过期
  var expirationTime = startTime + timeout;

  // 创建调度任务
  var newTask = {
    id: taskIdCounter++,
    // 真正的任务函数
    callback,
    // 任务优先级
    priorityLevel,
    // 任务开始的时间,表示任务何时才能执行
    startTime,
    // 任务的过期时间
    expirationTime,
    // 在小顶堆队列中排序的依据
    sortIndex: -1,
  };

  if (startTime > currentTime) {
    // 任务未过期,以开始时间作为 timerQueue 排序的依据
    newTask.sortIndex = startTime;
    // 加入 timerQueue 队列
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // taskQueue 中没有任务,且当前任务是 timerQueue 中排名最靠前的一个,
      // 说明需要检查 timerQueue 中有没有可以转移到 taskQueue 的任务
      if (isHostTimeoutScheduled) {
        // 即将调度新的 requestHostTimeout,先取消掉旧的
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // 调用 requestHostTimeout 实现任务转移,开启调度
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 任务已经过期,以过期时间作为 taskQueue 排序的依据
    newTask.sortIndex = expirationTime;
    // 加入 taskQueue 队列
    push(taskQueue, newTask);

    // 开始执行任务,使用 flushWork 去执行 taskQueue
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

堆操作与定时任务的相关方法:

  • pop:类似于数组的 pop() 方法。
  • peek:获取队列的第一项,与 pop 的区别是只获取不删除,类似于数组的 arr[0]。
  • push:向队列尾部加入一项,类似于数组的 push() 方法。
  • requestHostTimeout:一个定时器任务,时间间隔为「任务的开始时间 - 当前时间」,即剩余等待时间。
  • handleTimeout:调用 advanceTimers,检查 timerQueue 中过期的任务并放入 taskQueue。

4. requestHostCallback ​

packages/scheduler/src/forks/Scheduler.js

js
function requestHostCallback(callback) {
  // 过期任务请求调度
  scheduledHostCallback = callback;
  // 判断是否有 messageChannel 在运行
  if (!isMessageLoopRunning) {
    // 初始为 false
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

5. schedulePerformWorkUntilDeadline ​

通过 postMessage 主动向浏览器发送一条消息,收到消息说明浏览器空闲:

js
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
  port.postMessage(null);
};

6. performWorkUntilDeadline ​

判断 scheduledHostCallback 是否有值,有值则执行;执行完发现还有更多任务,就再次调用 schedulePerformWorkUntilDeadline 向浏览器发送消息判断是否空闲,直到任务被完全执行完:

js
const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // 记录开始时间,用于衡量主线程被阻塞的时长
    startTime = currentTime;
    const hasTimeRemaining = true;

    let hasMoreWork = true;
    try {
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      if (hasMoreWork) {
        // 还有任务,就在上一条消息结束时再调度下一条消息事件
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }
  // 让出主线程给浏览器绘制后,重置这个标记
  needsPaint = false;
};

scheduledHostCallback 在 requestHostCallback 中被赋值,最终指向 unstable_scheduleCallback 中传入的 flushWork,也就是 ensureRootIsScheduled 里 scheduleCallback 的回调。

7. flushWork ​

启动工作循环,处理所有待执行的任务:

js
try {
  return workLoop(hasTimeRemaining, initialTime);
} catch (error) {
  if (currentTask !== null) {
    const currentTime = getCurrentTime();
    markTaskErrored(currentTask, currentTime);
    currentTask.isQueued = false;
  }
  throw error;
}

8. workLoop ​

从任务队列 taskQueue 中取出任务,在 while 循环中处理每个任务,并且在每个任务开始之前检查是否应该中断。如果中断,performWorkUntilDeadline 中的 hasMoreWork 为 true,会再次去检查浏览器是否空闲:

js
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 当前任务尚未过期,但已经到达时间片上限,跳出循环让出主线程
      break;
    }
    const callback = currentTask.callback;
    if (typeof callback === "function") {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      if (enableProfiling) {
        markTaskRun(currentTask, currentTime);
      }
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === "function") {
        // 回调返回了新函数,说明任务还没做完,下次继续
        currentTask.callback = continuationCallback;
        if (enableProfiling) {
          markTaskYield(currentTask, currentTime);
        }
      } else {
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  // 返回是否还有额外任务
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

9. cancelCallback ​

ensureRootIsScheduled 中还有 cancelCallback 取消任务的操作。取消场景是:当前正在执行一个较低优先级的任务时,来了更高优先级的任务,需要先取消低优先级任务,再调度一个任务优先执行高优先级任务。

取消的方式是把任务的 callback 置为 null:

js
function unstable_cancelCallback(task) {
  if (enableProfiling) {
    if (task.isQueued) {
      const currentTime = getCurrentTime();
      markTaskCanceled(task, currentTime);
      task.isQueued = false;
    }
  }

  // 把 callback 置空以标记任务被取消;不能从队列中直接移除任意节点,
  // 因为基于数组的小顶堆只能移除堆顶元素
  task.callback = null;
}

workLoop 中会判断 callback 的类型,是函数则执行,否则直接把任务弹出。

四、同步调度 ​

1. performSyncWorkOnRoot ​

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

performSyncWorkOnRoot 是同步渲染的入口:先执行 render 阶段把 vdom 转成 fiber,再执行 commit 阶段更新到 DOM。

js
function performSyncWorkOnRoot(root) {
  // ...

  exitStatus = renderRootSync(root, lanes);

  // ...

  commitRoot(root);

  return null;
}

2. renderRootSync ​

renderRootSync 是同步模式的 render 入口:

  1. 不断地尝试执行 workLoopSync;
  2. 如果 workLoopSync 执行成功,则跳出循环;
  3. 如果执行过程中出现异常,则捕获后交由 handleError 处理。
js
function renderRootSync(root: FiberRoot, lanes: Lanes) {
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    // 创建 workInProgress
    prepareFreshStack(root, lanes);
    startWorkOnPendingInteractions(root, lanes);
  }

  do {
    try {
      // 工作轮询
      workLoopSync();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
}

3. workLoopSync ​

只要 workInProgress 不为空,就执行 performUnitOfWork。performUnitOfWork 负责处理单个 Fiber 节点,无论同步调度还是并发调度,都会走到该函数:

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

五、并发调度 ​

1. performConcurrentWorkOnRoot ​

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

performConcurrentWorkOnRoot 是并发渲染的入口,负责根据任务优先级和时间分片策略执行渲染任务:通过判断是否存在堵塞车道、过期车道以及是否超时,来决定是否开启时间分片。

js
function performConcurrentWorkOnRoot(root, didTimeout) {
  // includesBlockingLane(root, lanes):包含堵塞车道则不启用时间分片
  // includesExpiredLane(root, lanes):包含过期车道则不启用时间分片,防止饥饿问题
  // (disableSchedulerTimeoutInWorkLoop || !didTimeout):禁用调度超时或没有超时,才启用时间分片
  const shouldTimeSlice =
    !includesBlockingLane(root, lanes) &&
    !includesExpiredLane(root, lanes) &&
    (disableSchedulerTimeoutInWorkLoop || !didTimeout);

  // 根据 shouldTimeSlice 的值决定渲染模式
  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes) // 并发渲染
    : renderRootSync(root, lanes); // 同步渲染

  // ...

  // render 完成,进入 commit 阶段
  finishConcurrentRender(root, exitStatus, lanes);
}

如果 shouldTimeSlice 为 false,就调用 renderRootSync 进行同步 render。

提示:点击这类离散事件会被分配为同步车道,在 ensureRootIsScheduled 中即被判定为同步更新,并在下一个微任务中执行;setTimeout 中的更新会被分配为默认车道,通过 scheduleCallback 调度 performConcurrentWorkOnRoot,但由于是堵塞车道,shouldTimeSlice 为 false,最终会在浏览器空闲时使用 renderRootSync 同步渲染。默认车道的过期时间是 5s,浏览器是否空闲每 5ms 判断一次。

在 React 18 中,只有明确标记为并发、且不包含堵塞车道的更新,才会使用 renderRootConcurrent 进行并发渲染。

2. renderRootConcurrent ​

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

renderRootConcurrent 是并发模式的 render 入口,通过 workLoopConcurrent 执行可中断的渲染工作:

js
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
  // 上下文管理
  const prevExecutionContext = executionContext;
  executionContext |= RenderContext;
  const prevDispatcher = pushDispatcher();

  // 2. 工作栈重置条件判断:根节点有变化或车道有更新,
  // 丢弃现有栈重新准备一份,否则从上次中断处继续
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    workInProgressTransitions = getTransitionsForLanes(root, lanes);
    resetRenderTimer();
    prepareFreshStack(root, lanes); // 核心重置方法
  }

  if (enableSchedulingProfiler) {
    markRenderStarted(lanes);
  }

  do {
    try {
      // 驱动 Fiber 处理
      workLoopConcurrent();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);

  // 5. 清理阶段,上下文重置
  resetContextDependencies();

  popDispatcher(prevDispatcher);
  executionContext = prevExecutionContext;

  if (workInProgress !== null) {
    // 还有剩余工作,未渲染完成
    if (enableSchedulingProfiler) {
      markRenderYielded();
    }
    return RootInProgress;
  } else {
    // 整棵树处理完成
    if (enableSchedulingProfiler) {
      markRenderStopped();
    }

    // 置空以标记当前没有进行中的 render
    workInProgressRoot = null;
    workInProgressRootRenderLanes = NoLanes;

    // 返回渲染最终状态
    return workInProgressRootExitStatus;
  }
}

3. workLoopConcurrent ​

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

如果 workInProgress 未渲染完成且不需要打断,就使用 performUnitOfWork 处理单个 Fiber 节点:

js
function workLoopConcurrent() {
  // 在未超时且存在待处理 Fiber 时持续工作
  // 通过 shouldYield() 与 Scheduler 协作,当浏览器需要处理高优先级任务时暂停工作
  while (workInProgress !== null && !shouldYield()) {
    // 每次循环执行 performUnitOfWork 处理单个 Fiber 节点
    performUnitOfWork(workInProgress);
  }
}

提示:最终还是和 React 17 一样使用 performUnitOfWork 处理单个 Fiber 节点,只是多了一个 shouldYield() 判断是否中断。

shouldYield 就是 scheduler 文件中的 shouldYieldToHost 函数。

4. shouldYield ​

packages/scheduler/src/forks/SchedulerPostTask.js

shouldYield 实际上是 unstable_shouldYield,判断当前时间是否超过了 deadline:

js
export function unstable_shouldYield() {
  return getCurrentTime() >= deadline;
}

deadline 的值在同文件的 runTask 中被赋值,每次调用都会根据当前时间加 5ms:

js
deadline = getCurrentTime() + yieldInterval; // yieldInterval 为 5

即 React 18 处理并发任务时,会定期每 5ms 主动让出主线程的控制权。

优先级的高低只影响 Scheduler 中 taskQueue 的排序结果,是否打断只取决于过期时间。

六、执行单元 ​

1. performUnitOfWork ​

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

performUnitOfWork 位于 Render 阶段的中间层,是工作循环函数调用的核心处理单元,用于处理单个 Fiber 节点。

它会调用 beginWork 处理当前 Fiber 节点并计算下一个节点:如果没有下一个节点,说明处理完成,调用 completeUnitOfWork;如果有下一个节点,把 next 赋值给 workInProgress,由 workLoopConcurrent 或 workLoopSync 中的 while 循环继续处理。

js
function performUnitOfWork(unitOfWork: Fiber): void {
  // ...

  next = beginWork(current, unitOfWork, subtreeRenderLanes);

  if (next === null) {
    // 没有新的 work,完成当前工作
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }

  ReactCurrentOwner.current = null;
}

2. beginWork ​

packages/react-reconciler/src/ReactFiberBeginWork.old.js

负责把 vdom 转成 fiber,不同节点的 reconcile 逻辑不同:函数组件调用 updateFunctionComponent,class 组件调用 updateClassComponent。

js
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...

  case FunctionComponent: {
    const Component = workInProgress.type;
    const unresolvedProps = workInProgress.pendingProps;
    const resolvedProps =
      workInProgress.elementType === Component
        ? unresolvedProps
        : resolveDefaultProps(Component, unresolvedProps);
    return updateFunctionComponent(
      current,
      workInProgress,
      Component,
      resolvedProps,
      renderLanes,
    );
  }
  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,
    );
  }
}

3. updateFunctionComponent ​

packages/react-reconciler/src/ReactFiberBeginWork.old.js

函数组件会被调用,拿到 render 出的 vdom 后继续做 reconcile:

js
function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes,
) {
  // 构建 hooks
  nextChildren = renderWithHooks(
    current,
    workInProgress,
    Component,
    nextProps,
    context,
    renderLanes,
  );
  // 生成 Fiber 链表
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}

4. renderWithHooks ​

渲染 Hooks 的流程可参考 V16.13.1 的实现。

5. reconcileChildren ​

packages/react-reconciler/src/ReactFiberBeginWork.old.js

current 指当前渲染的树,通过判断 current 的值可知是否需要 diff:

js
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderExpirationTime: ExpirationTime,
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderExpirationTime,
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderExpirationTime,
    );
  }
}

6. ChildReconciler ​

packages/react-reconciler/src/ReactChildFiber.old.js

mountChildFibers 和 reconcileChildFibers 都是 ChildReconciler 的返回值,只是传参不同:

js
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

ChildReconciler 负责把 vdom 转成 Fiber,并进行 diff 操作:

js
function ChildReconciler(shouldTrackSideEffects) {
  // ...

  // 负责把 vdom 转成 Fiber
  function createChild() {}

  // 函数入口
  function reconcileChildFibers() {}
  return reconcileChildFibers;
}

performUnitOfWork 会判断其返回值是否为 null:为 null 说明执行完毕,调用 completeUnitOfWork,render 阶段结束;不为 null 则把返回值赋给 workInProgress,等待下一次处理。

提示:同步任务中 workLoopSync 会执行完所有 performUnitOfWork,即 render 阶段不可打断;并发任务中 workLoopConcurrent 是可被打断的,会在浏览器空闲时再次执行。

七、commit 阶段 ​

1. finishConcurrentRender ​

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

与 performSyncWorkOnRoot 相比,performConcurrentWorkOnRoot 在 commit 阶段会多调用一个 finishConcurrentRender,因为新增了乐观更新功能,可以回退 UI。

finishConcurrentRender 根据 exitStatus 的不同状态执行不同逻辑:

  • RootErrored:渲染过程中发生错误,直接调用 commitRoot 提交错误状态。
  • RootSuspended:渲染被挂起(例如等待数据加载),某些条件下延迟调用 commitRoot,或立即调用 commitRoot 提交回退 UI(乐观更新)。
  • RootSuspendedWithDelay:与 RootSuspended 类似,但会根据延迟时间决定是否提交回退 UI(乐观更新)。
  • RootCompleted:渲染完成,调用 commitRoot 提交最终的 UI 更新。
js
function finishConcurrentRender(root, exitStatus, lanes) {
  switch (exitStatus) {
    // ...
    case RootCompleted: {
      // 工作已完成,可以提交
      commitRoot(
        root,
        workInProgressRootRecoverableErrors,
        workInProgressTransitions,
      );
      break;
    }
    default: {
      throw new Error("Unknown root exit status.");
    }
  }
}

2. commitRoot ​

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

setCurrentUpdatePriority(DiscreteEventPriority) 中 DiscreteEventPriority 的值为 SyncLane,即把当前更新任务设置为同步任务。这表明 commitRoot 总是同步完成,不会被中断。

js
function commitRoot(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
) {
  // 1. 保存当前优先级和过渡状态
  const previousUpdateLanePriority = getCurrentUpdatePriority();
  const prevTransition = ReactCurrentBatchConfig.transition;

  try {
    // 2. 设置提交阶段的优先级和过渡状态
    ReactCurrentBatchConfig.transition = null;
    setCurrentUpdatePriority(DiscreteEventPriority);
    // 3. 执行实际的提交工作
    commitRootImpl(
      root,
      recoverableErrors,
      transitions,
      previousUpdateLanePriority,
    );
  } finally {
    // 4. 恢复之前的优先级和过渡状态
    ReactCurrentBatchConfig.transition = prevTransition;
    setCurrentUpdatePriority(previousUpdateLanePriority);
  }

  return null;
}

3. commitRootImpl ​

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

commit 也分为三个小阶段:Before Mutation 阶段、Mutation 阶段、Layout 阶段。

  • before mutation:在 DOM 操作之前执行。
  • mutation 阶段:调用 commitMutationEffects,根据 Fiber 节点的类型和副作用标志执行相应的 DOM 操作。
  • layout 阶段:调用 commitLayoutEffects,在 DOM 变更后执行布局相关的副作用,ref 的更新与 useLayoutEffect 的回调都在此阶段执行。

提示:useEffect 的回调在 commit 阶段完成、浏览器绘制之后异步执行。

js
// 1. Before Mutation 阶段
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
  root,
  finishedWork,
);

// 2. Mutation 阶段
commitMutationEffects(root, finishedWork, lanes);

// 3. Layout 阶段
commitLayoutEffects(finishedWork, root, lanes);

// 请求绘制
requestPaint();

// 处理剩余被中断的低优先级更新,安排下一次渲染
ensureRootIsScheduled();

提示:requestPaint 会把 needsPaint 置为 true,shouldYieldToHost 中据此判断需要让出主线程。即如果有新的渲染任务,React 会打断当前流程,把主线程让给浏览器进行绘制。