陌上人如玉
公子世无双

一、全景调用链(一张图看完)

点击按钮 → setState(= dispatchSetState)
   │
   ├─ ① 建更新、入队
   │     requestEventTime()            取「当前事件时间」
   │     requestUpdateLane(fiber)      选一个「车道(优先级)」
   │     createUpdate(eventTime, lane)
   │     enqueueUpdate(fiber, update)  把 update 挂到 fiber 的 updateQueue
   │
   └─ ② 调度
         scheduleUpdateOnFiber(root, fiber, lane, eventTime)
            ├─ checkIfNestedUpdatePriority(...)      嵌套更新检测(render 中更新?)
            ├─ markRootUpdated(root, lane, eventTime) 记录「根上有待处理车道」
            └─ ensureRootIsScheduled(root, eventTime)
                  ├─ getNextLanes(root)              算出「这一轮要处理哪些车道」
                  ├─ 是 SyncLane?
                  │    └─ scheduleMicrotask(flushSyncCallbacks)   → 微任务里同步渲染
                  └─ 否
                       ├─ lanesToEventPriority(lanes) 车道 → Scheduler 优先级
                       └─ scheduleCallback(priority, performConcurrentWorkOnRoot.bind(null, root))
                              └─ Scheduler(scheduler 包)
                                   push(taskQueue) → requestHostCallback(flushWork)
                                   → MessageChannel.port2.postMessage(null)
                                   → performWorkUntilDeadline
                                        while (!shouldYieldToHost()) { task.callback() }   ⭐ 时间片

   ■ 渲染阶段(可中断)
         performConcurrentWorkOnRoot(root, didTimeout)
            ├─ getNextLanes / shouldTimeSlice 判断
            ├─ renderRootConcurrent(root, lanes)
            │     ├─ prepareFreshStack(root, lanes)    建 workInProgress
            │     └─ workLoopConcurrent()
            │            while (workInProgress !== null && !shouldYield()) {
            │                performUnitOfWork(workInProgress)
            │            }
            │                ├─ beginWork(...)    → 返回 child(「递」)
            │                └─ completeUnitOfWork → completeWork(「归」)+ sibling/return
            └─ 若本帧没跑完 → 返回「继续函数」,让 Scheduler 再调度一次

   ■ 提交阶段(不可中断)
         commitRoot(root) → commitRootImpl(root, ...)
            ├─ flushPassiveEffects()                先清上一次的 useEffect
            ├─ commitBeforeMutationEffects()        getSnapshotBeforeUpdate / 调度 passive
            ├─ commitMutationEffects()              ⭐ 改 DOM(内部先跑 useInsertionEffect)
            ├─ root.current = finishedWork          ⭐ 双缓存切换
            ├─ commitLayoutEffects()                ⭐ 赋 ref + 跑 useLayoutEffect
            └─ 调度 passive effects(微任务里跑 useEffect)

二、阶段 ①:updateContainer —— 建立并登记一次更新

// ✅ React 源码结构(简化,保留关键步骤与命名)
function updateContainer(element, container, parentComponent, callback) {
  const current = container.current;              // HostRootFiber
  const eventTime = requestEventTime();           // 「现在」
  const lane = requestUpdateLane(current);        // ⭐ 选车道(优先级)

  // ① 建「更新对象」
  const update = createUpdate(eventTime, lane);
  update.payload = { element };                   // render(x) 的 x
  if (callback) update.callback = callback;

  // ② 入队:挂到 fiber.updateQueue(环形链表)
  enqueueUpdate(current, update, lane);

  // ③ 调度
  const root = scheduleUpdateOnFiber(current, lane, eventTime);
  return lane;
}
⭐⭐ 三个关键点(每一步都值得理解):

① 【`requestEventTime()` —— 为什么要「事件时间」?】
   它返回「触发这次更新的时间点」(同一批事件里的更新会共享同一个 eventTime)。
   用途:计算「过期时间」——低优先级更新「等太久」时会「过期」,
   过期后会被【强制同步执行】(避免无限期饿死)。
   → 这就解释了「为什么低优先级任务最终一定会执行」:因为它们会过期。

② 【`requestUpdateLane(fiber)` —— 谁决定优先级?】
   它根据「当前上下文」选车道:
   · 在 React 事件处理里(离散事件)→ SyncLane / InputContinuousLane
   · 在 startTransition 里 → TransitionLanes
   · 其它 → DefaultLane
   → 这是「点击里的 setState 表现得像同步」的**真正原因**(不是「React 特殊照顾点击」,
     而是「点击被分配了 SyncLane」)。

③ 【`enqueueUpdate` —— 更新存在哪里?】
   存在 `fiber.updateQueue` 上(一个「环形链表」,带 `shared.pending` 指针)。
   用环形链表是为了「O(1) 追加」+「方便在 render 时一次性切断消费」。

三、阶段 ②:scheduleUpdateOnFiber 与 ensureRootIsScheduled

function scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
  // ① 检查「是否在 render 中触发了更新」(嵌套更新)→ 用于「同步冲洗」避免撕裂
  checkIfNestedUpdatePriority(root, fiber);

  // ② 在「根」上标记:有新的车道需要处理
  markRootUpdated(root, lane, eventTime);

  // ③ 确保「已经安排了一次调度」(去重:同一批更新只调度一次)
  ensureRootIsScheduled(root, eventTime);
}
function ensureRootIsScheduled(root, currentTime) {
  // ① 取出「根上所有待处理车道」中「这一轮要处理的那些」
  const nextLanes = getNextLanes(root, root.pendingLanes);
  if (nextLanes === NoLanes) return;                 // 没活干了

  // ② 取「最高优先级的那一条车道」
  const newCallbackPriority = getHighestPriorityLane(nextLanes);

  // ③ 如果「已经有一个同样优先级的调度在进行」→ 直接复用,不重复调度
  const existingCallbackPriority = root.callbackPriority;
  if (existingCallbackPriority === newCallbackPriority) return;

  // ④ 按优先级分两条路
  if (newCallbackPriority === SyncLane) {
    // ⭐ 同步车道:用微任务(而不是「立刻执行」)
    scheduleMicrotask(flushSyncCallbacks);
  } else {
    // ⭐ 其它车道:交给 Scheduler(可中断)
    const schedulerPriorityLevel = lanesToEventPriority(nextLanes);
    scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root)
    );
  }
}
⭐⭐⭐ 三个「为什么」:

① 【为什么 SyncLane 用 `scheduleMicrotask` 而不是直接调用渲染?】
   因为「同步渲染」发生在「事件处理函数返回之后」——
   如果直接调用,就会出现「setState 立刻渲染」的语义,
   那样「一次点击里的多次 setState」就不再批处理了。
   用微任务 → 等当前同步代码跑完 → 再统一渲染 → **这就是批处理的实现方式**。

② 【为什么用 `lanesToEventPriority` 做「车道 → Scheduler 优先级」的映射?】
   因为「车道」是 React 自己的优先级体系(用于「能否合并」「能否被打断」),
   而「Scheduler 优先级」是给调度器用的(决定「用不用时间片、让不让路」)。
   两套体系需要翻译:Sync 车道 → 同步路径;Transition 车道 → Low/Idle 优先级
   (所以「过渡更新」在 Scheduler 眼里是「低优先级」→ 更容易被让路)。

③ 【为什么要「去重」(`existingCallbackPriority` 判断)?】
   同一个优先级已经有调度在排队时,再排一次没意义(会跑两遍)。
   → 这也是「一个事件循环里 100 次 setState 只渲染一次」的机制之一。

四、Scheduler:时间片是怎么实现的

// ✅ scheduler 包的核心(简化,保留关键命名)
const frameYieldMs = 5;                     // ⭐ 时间片长度(约 5ms)

let taskQueue = [];                         // 按 expirationTime 排序的小顶堆
let currentTask = null;
let taskIdCounter = 1;

let startTime = -1;
let currentTime = -1;
let deadline = 0;

function scheduleCallback(priorityLevel, callback, options) {
  const currentTime = getCurrentTime();
  const startTime = currentTime;
  const timeout = timeoutByPriority[priorityLevel];    // 不同优先级不同超时

  const expirationTime = startTime + timeout;
  const newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };

  push(taskQueue, newTask);                  // 入堆
  requestHostCallback(flushWork);            // ⭐ 请求「宿主」在合适时机执行
  return newTask;
}
// ⭐⭐ 关键的「宿主调度」:用 MessageChannel 而不是 requestIdleCallback
const channel = new MessageChannel();
const port = channel.port2;

function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();       // 默认实现:port.postMessage(null)
  }
}

channel.port1.onmessage = performWorkUntilDeadline;

function performWorkUntilDeadline() {
  const currentTime = getCurrentTime();

  // ⭐ 设置「这一帧的时间片 deadline」
  startTime = currentTime;
  deadline = currentTime + frameYieldMs;      // ≈ 5ms 之后必须让出

  const hasMoreWork = scheduledHostCallback?.(true);
  if (hasMoreWork) {
    schedulePerformWorkUntilDeadline();        // 还有活 → 再排一个消息,下一轮继续
  } else {
    isMessageLoopRunning = false;
  }
}

function shouldYieldToHost() {
  return getCurrentTime() >= deadline;         // ⭐ 时间片用完 → 让出主线程
}
⭐⭐⭐ 为什么用 `MessageChannel` 而不用 `requestIdleCallback` / `requestAnimationFrame`?

① `requestIdleCallback`:
   · 兼容性差(Safari 长期不支持)
   · 调用频率被限制(相邻回调之间的间隔可能很长,规范允许「最多 ~20 次/秒」)
   · 「空闲时间」由浏览器决定,**不可控**——React 需要「稳定的 5ms 时间片」
   → 所以它只适合「真的不急」的任务,不适合 React 的调度

② `requestAnimationFrame`:
   · 只在「页面可见」时才触发(**后台标签页会暂停** → 更新被挂起)
   · 一帧只调一次(粒度太粗)
   → 适合动画,不适合「需要多次让出」的调度

③ `setTimeout`:
   · 有「最小延迟」限制(嵌套 5 层后至少 4ms)
   · 精度差、且在后台标签页被节流到 1s
   → 达不到「5ms 时间片」的精度要求

④ `MessageChannel`:
   · 是「宏任务」,但**不需要等待固定延迟**(比 setTimeout 更及时)
   · 在所有主流浏览器可用(包括 Worker 里)
   · 可以「一次任务里多次 postMessage」来「连续让出」
   → ⭐ 因此成为 Scheduler 的默认实现
⭐ 一个「传得很广但不准确」的说法要澄清:

   ❌ 「React 用 requestIdleCallback 做时间切片」
   ✅ React 用的是「MessageChannel + 自己的 5ms 时间片」;
      `requestIdleCallback` 只在「Scheduler 的某个降级路径」里出现(且不是默认)。

五、阶段 ③:可中断的渲染循环

function performConcurrentWorkOnRoot(root, didTimeout) {
  const lanes = getNextLanes(root, root.pendingLanes);
  if (lanes === NoLanes) return null;

  // ⭐ 判断「是否可以用时间切片(并发渲染)」
  const shouldTimeSlice =
    !includesBlockingLane(lanes) &&        // 不含「阻塞车道」(Sync / InputContinuous)
    !includesExpiredLane(root, lanes) &&   // 没有「已过期」的车道(过期必须同步做完)
    !didTimeout;                           // Scheduler 没告诉我们「超时了」

  const exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);

  if (exitStatus !== RootInProgress) {
    // 渲染完成 → 提交
    commitRoot(root, /* ... */);
  }

  // ⭐ 返回「继续函数」:如果本帧没跑完,Scheduler 会用这个函数再调度一次
  return root.callbackNode === originalCallbackNode
    ? performConcurrentWorkOnRoot.bind(null, root)
    : null;
}
function renderRootConcurrent(root, lanes) {
  prepareFreshStack(root, lanes);            // 建 workInProgress 树(双缓存)
  workLoopConcurrent();
  // ...
}

function workLoopConcurrent() {
  // ⭐⭐ 可中断的关键:每处理一个单元就检查「是否该让出」
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
  // 循环退出时:
  // · workInProgress === null  → 整棵树处理完了
  // · shouldYield() === true   → 让出主线程(下次从这里继续)
}

function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
  const next = beginWork(current, unitOfWork, renderLanes);   // ⭐「递」

  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    completeUnitOfWork(unitOfWork);                            // ⭐ 没有子节点 → 「归」
  } else {
    workInProgress = next;                                     // 进入子节点
  }
}

function completeUnitOfWork(unitOfWork) {
  let completedWork = unitOfWork;
  do {
    completeWork(current, completedWork, renderLanes);         // 处理「完成」逻辑
    // 收集副作用(flags)到父节点(subtreeFlags)
    // ...
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;                           // 处理兄弟
      return;
    }
    completedWork = completedWork.return;                      // 回到父节点
    workInProgress = completedWork;
  } while (completedWork !== null);

  // 回到根 → 渲染阶段结束
}
⭐⭐ 「可中断」的两个前提(结合 4.2 理解):

① 【遍历是「链表 + 循环」而不是递归】
   `workInProgress` 只是一个「全局指针」——所以可以随时「保存并返回」。
   递归的调用栈无法保存,这是 Fiber 用链表的核心原因。

② 【「是否让出」是「每个工作单元检查一次」】
   `while (workInProgress !== null && !shouldYield())`
   → 粒度是「一个组件/一个节点」的渲染,通常远小于 5ms。

⚠️ 注意:**commit 阶段不在这个循环里**——它由 `commitRoot` 单独完成,不可中断。

六、阶段 ④:commitRootImpl —— 不可中断的提交

function commitRootImpl(root, recoverableErrors, transitions) {
  // ① 先把「上一次的 passive effects」清掉(避免顺序错乱)
  do {
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);

  const finishedWork = root.finishedWork;
  if (finishedWork === null) return;
  root.finishedWork = null;

  // ② before mutation:读取 DOM 快照(getSnapshotBeforeUpdate)、调度 passive effects
  commitBeforeMutationEffects(root, finishedWork);

  // ③ mutation:⭐ 把变更应用到 DOM(内部先跑 useInsertionEffect)
  commitMutationEffects(root, finishedWork, lanes);

  // ⭐⭐ 双缓存切换:current 指向新的树(在 layout 之前完成)
  root.current = finishedWork;

  // ④ layout:赋 ref、执行 useLayoutEffect、触发 componentDidMount/DidUpdate
  commitLayoutEffects(finishedWork, root, lanes);

  // ⑤ 如果本次有 passive effects → 调度它们(异步执行 useEffect)
  if (rootDoesHavePassiveEffects) {
    rootWithPendingPassiveEffects = root;
  }

  // ⑥ 冲洗「同步回调队列」(flushSync 与 SyncLane 的任务)
  flushSyncCallbacks();
}
子阶段 做什么 典型案例
before mutation 读 DOM 快照 getSnapshotBeforeUpdate(类组件)
mutation 改 DOM(插入/更新/删除)+ 执行 useInsertionEffect <div> 属性更新、节点删除
双缓存切换 root.current = finishedWork 后续的读操作看到「新树」
layout 赋 ref、执行 useLayoutEffect、componentDidMount/DidUpdate 测量、焦点、同步副作用
passive(异步) 执行 useEffect 请求、订阅、日志
⭐⭐ 三个「顺序为什么是这样」的问题(面试常问):

① 【为什么 `useInsertionEffect` 在 mutation 里、而 `useLayoutEffect` 在 layout 里?】
   insertion 要在「DOM 变更之前」把样式插进样式表(让浏览器一次算对布局),
   layout 要在「DOM 变更之后」读/写布局(此时 DOM 已是新的)。
   → 参见 2.17

② 【为什么 `root.current = finishedWork` 放在 mutation 之后?】
   因为在 mutation 阶段,「新树的 DOM」正在被写入真实文档,
   此时读取操作(如 `getSnapshotBeforeUpdate`、布局相关逻辑)仍然应该看到「旧树」;
   等 DOM 变更完成后再切换 `current`,后续(layout 阶段)的逻辑才看到新树。
   → 这个「切换时机」是「双缓存 + 副作用顺序」的关键衔接点。

③ 【为什么 commit 不能中断?】
   因为「DOM 变更做到一半」会让用户看到不一致的界面
   (列表删了一半、样式应用了一部分)。
   → React 的取舍是「render 可以慢慢算(可中断),commit 必须一口气做完」。

七、完整实现:一个 mini Scheduler + 可中断渲染(210 行)

下面把「时间片 + 让出 + 可中断工作循环」这三件事用 210 行实现出来,可以直接在浏览器里跑并观察「让出主线程」。

// mini-scheduler.ts —— 教学版 Scheduler(复现 MessageChannel + 时间片 + 让出)
type PriorityLevel = 'immediate' | 'userBlocking' | 'normal' | 'low' | 'idle';

const TIMEOUT_BY_PRIORITY: Record<PriorityLevel, number> = {
  immediate: -1,        // 立即(同步)
  userBlocking: 250,    // 需要快速响应(如输入)
  normal: 5000,
  low: 10000,
  idle: 1073741823,     // 尽量延后
};

interface Task {
  id: number;
  callback: (didTimeout: boolean) => boolean | void;
  priorityLevel: PriorityLevel;
  expirationTime: number;
  sortIndex: number;
}

let taskQueue: Task[] = [];
let taskIdCounter = 0;
let isMessageLoopRunning = false;
let scheduledHostCallback: ((didTimeout: boolean) => boolean) | null = null;

/** ⭐ 时间片长度(与 React 的 frameYieldMs = 5 一致) */
const FRAME_YIELD_MS = 5;
let startTime = 0;
let deadline = 0;
let currentTime = 0;

const getCurrentTime = () => performance.now();

/** 小顶堆(按 expirationTime 排序)——这里用简化版:插入后排序 */
function push(queue: Task[], task: Task) {
  task.sortIndex = task.expirationTime;
  queue.push(task);
  queue.sort((a, b) => a.sortIndex - b.sortIndex);
}
function peek(queue: Task[]): Task | undefined {
  return queue[0];
}
function pop(queue: Task[]): Task | undefined {
  const t = queue.shift();
  return t;
}

/** ⭐⭐ 宿主调度:用 MessageChannel(与 React 的选择一致) */
const channel = typeof MessageChannel !== 'undefined' ? new MessageChannel() : null;

function schedulePerformWorkUntilDeadline() {
  if (channel) {
    channel.port2.postMessage(null);          // → 触发 port1.onmessage
  } else {
    setTimeout(performWorkUntilDeadline, 0);   // Node/测试环境降级
  }
}

if (channel) channel.port1.onmessage = () => performWorkUntilDeadline();

function performWorkUntilDeadline() {
  currentTime = getCurrentTime();
  startTime = currentTime;
  deadline = currentTime + FRAME_YIELD_MS;    // ⭐ 设置本帧的时间片

  const hasMoreWork = scheduledHostCallback?.(true);

  if (hasMoreWork) {
    schedulePerformWorkUntilDeadline();        // 还有活 → 再排一次(下一帧继续)
  } else {
    isMessageLoopRunning = false;
    scheduledHostCallback = null;
  }
}

/** ⭐ 让出判断:时间片用完了吗 */
export function shouldYieldToHost(): boolean {
  return getCurrentTime() >= deadline;
}

/** 对外 API:安排一个任务 */
export function scheduleCallback(
  priorityLevel: PriorityLevel,
  callback: (didTimeout: boolean) => boolean | void
): Task {
  const time = getCurrentTime();
  const expirationTime = priorityLevel === 'immediate' ? -1 : time + TIMEOUT_BY_PRIORITY[priorityLevel];

  const task: Task = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    expirationTime,
    sortIndex: -1,
  };

  push(taskQueue, task);
  requestHostCallback(flushWork);
  return task;
}

function requestHostCallback(callback: (didTimeout: boolean) => boolean) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

/** ⭐ 工作循环:在时间片内不断取任务执行,超时就返回「还有活」 */
function flushWork(didTimeout: boolean): boolean {
  currentTime = getCurrentTime();
  startTime = currentTime;
  deadline = currentTime + FRAME_YIELD_MS;

  return workLoop(didTimeout);
}

function workLoop(didTimeout: boolean): boolean {
  let currentTask = peek(taskQueue);

  while (currentTask !== null) {
    // ⭐ 如果时间片用完,且任务没超时 → 让出主线程(下次继续)
    if (shouldYieldToHost() && currentTask.expirationTime > currentTime) {
      break;
    }

    const callback = currentTask.callback;
    const didTimeoutForTask = currentTask.expirationTime <= currentTime;

    if (typeof callback === 'function') {
      currentTask.callback = () => {};          // 先清掉,避免重入
      const continuation = callback(didTimeoutForTask || didTimeout);

      if (typeof continuation === 'function') {
        // ⭐ 任务「没做完」→ 用返回的「继续函数」替换回调,等下一帧再执行
        currentTask.callback = continuation as unknown as (t: boolean) => boolean | void;
      } else {
        pop(taskQueue);                         // 任务完成
      }
    } else {
      pop(taskQueue);
    }

    currentTask = peek(taskQueue);
  }

  return currentTask !== null;                  // 还有任务 → 告诉宿主「继续排」
}
// ✅ 用一个「可中断的渲染任务」验证:观察它被切成多片
import { scheduleCallback, shouldYieldToHost } from './mini-scheduler';

/** 模拟「一棵 fiber 树的工作单元」(用数组代替链表,逻辑一样) */
interface Unit {
  id: number;
  cost: number;        // 这个单元的「渲染耗时」(用忙等模拟)
}

function buildUnits(count: number, costEach: number): Unit[] {
  return Array.from({ length: count }, (_, i) => ({ id: i, cost: costEach }));
}

function busyWait(ms: number) {
  const end = performance.now() + ms;
  // 微小的忙等(模拟「渲染一个组件」的同步计算)
  while (performance.now() < end) { /* spin */ }
}

/** ⭐ 可中断的「渲染」:每处理一个单元就检查「是否该让出」 */
export function runInterruptibleWork(units: Unit[], onProgress: (done: number) => void) {
  let index = 0;
  const total = units.length;
  const t0 = performance.now();

  const workLoop = (): (() => boolean | void) | void => {
    while (index < total && !shouldYieldToHost()) {
      busyWait(units[index]!.cost);              // 模拟渲染这个单元
      index++;
    }

    onProgress(index);

    if (index < total) {
      console.log(`[mini] 让出主线程(已处理 ${index}/${total},耗时 ${(performance.now() - t0).toFixed(1)}ms)`);
      return workLoop;                            // ⭐ 返回「继续函数」→ 下一帧接着跑
    }

    console.log(`[mini] 全部完成(总耗时 ${(performance.now() - t0).toFixed(1)}ms,被切成多片)`);
  };

  scheduleCallback('normal', workLoop);
}

/** ============ 在页面上验证 ============ */
export function SchedulerDemo() {
  return (
    <button
      onClick={() => {
        const units = buildUnits(300, 0.3);        // 300 个单元 × 0.3ms ≈ 90ms
        runInterruptibleWork(units, (done) => {
          document.title = `已处理 ${done}`;        // 边跑边更新(说明主线程没被完全占用)
        });
      }}
    >
      跑可中断渲染(观察控制台的分片日志)
    </button>
  );
}
⭐⭐ 跑起来你会看到(控制台):

   [mini] 让出主线程(已处理 17/300,耗时 5.2ms)
   [mini] 让出主线程(已处理 34/300,耗时 10.4ms)
   ...
   [mini] 全部完成(总耗时 92.3ms,被切成多片)

⭐ 三个「与 React 一致」的观察点:
   ① 每个时间片约 5ms(`FRAME_YIELD_MS`)
   ② 「让出」是通过「返回一个继续函数」实现的——Scheduler 把它存回任务,
      下一帧再调用(真实 React 里也是这个模式:`performConcurrentWorkOnRoot`
      返回「自己」表示「还没做完」)
   ③ 因为「让出」依赖 MessageChannel(宏任务),
      期间浏览器有机会处理输入/绘制 —— 所以「长渲染不阻塞交互」

八、本篇特有的坑

① 认为「React 用 requestIdleCallback 做时间切片」
   → 实际是「MessageChannel + 5ms 时间片」;rIC 只在降级路径出现

② 认为「setState 之后立刻渲染」
   → SyncLane 走「微任务」→ 当前同步代码跑完才渲染(这是批处理的实现)

③ 认为「所有更新都可中断」
   → SyncLane / InputContinuousLane(含 blocking lane)与「已过期」的车道
     会走 `renderRootSync`,【不可中断】

④ 认为「commit 也能被打断」
   → `commitRoot` 是独立调用,不在 workLoop 里(不可中断)

⑤ 以为「时间片固定 5ms 一次」
   → 它是「每一帧重新设置 deadline」;且 `flushWork` 每次进入都会重置时间片

⑥ 认为「低优先级任务可能永远不执行」
   → 它们有「过期时间」(`expirationTime`)→ 过期后会被同步完成(防饿死)

⑦ 把「车道(Lane)」与「Scheduler 优先级」混为一谈
   → 两套体系:车道是 React 内部的(能否合并/中断),
     Scheduler 优先级是给调度器的(用不用时间片)——中间靠 `lanesToEventPriority` 翻译

⑧ 以为「enqueueUpdate 只是 push 到数组」
   → 是「环形链表 + pending 指针」(为了 O(1) 追加与整批消费)

⑨ 认为「`root.current = finishedWork` 在 commit 最开始」
   → 它在 mutation 之后、layout 之前

⑩ 用 `setTimeout(fn, 0)` 模拟「让出主线程」
   → 嵌套多层后会有 4ms 最小延迟,精度差;后台标签页还会被节流到 1s

⑪ 以为「workInProgress 是一个局部变量」
   → 它是「当前工作单元指针」(可以是模块级/全局的),这正是「可暂停」的前提

⑫ 忽略「render 阶段可能被『重放』」
   → 所以 render 必须纯(否则副作用会重复)——这是 StrictMode 双调用的理论基础

⑬ 以为「Scheduler 在 Worker 里跑」
   → Scheduler 与 React 渲染都在【主线程】;Worker 只用于「把用户的重计算移出去」

⑭ 认为「`flushSyncCallbacks` 只在 flushSync 时用」
   → 它同时用于「SyncLane 的微任务冲洗」与「commit 结束后的同步回调」

九、面试延伸

  1. 「说一下 setState 之后到界面更新的完整链路。」
    ① 建更新:requestEventTime() 取时间、requestUpdateLane() 选车道、createUpdate() 建更新对象、enqueueUpdate() 把它挂到 fiber.updateQueue(环形链表);② 调度:scheduleUpdateOnFiber() → markRootUpdated() 在根上标记车道 → ensureRootIsScheduled() 用 getNextLanes() 算出待处理车道并去重调度;③ 分路:若是 SyncLane → scheduleMicrotask(flushSyncCallbacks)(微任务里同步渲染,这就是批处理);否则 → lanesToEventPriority() 翻译成 Scheduler 优先级 → scheduleCallback(performConcurrentWorkOnRoot);④ 渲染(renderRootConcurrent + workLoopConcurrent,每处理一个 fiber 检查 shouldYield(),可中断);⑤ 提交(commitRootImpl:flushPassiveEffects → commitBeforeMutationEffects → commitMutationEffects → root.current = finishedWork → commitLayoutEffects → 调度 passive effects)。

  2. 「Scheduler 为什么用 MessageChannel 而不是 requestIdleCallback?」
    因为 requestIdleCallback 有三个致命问题:① 兼容性差(Safari 长期不支持);② 调用频率被限制(规范允许「最多约 20 次/秒」,相邻回调间隔可能很长);③ 「空闲时间」由浏览器决定,不可控——而 React 需要稳定的 5ms 时间片来「让出后及时恢复」。requestAnimationFrame 的问题是「后台标签页会暂停」且「一帧只调一次」;setTimeout 有「嵌套 4ms 最小延迟」且后台被节流到 1s。而 MessageChannel 是宏任务(比 setTimeout 更及时)、所有主流浏览器都支持,所以成为默认实现。

  3. 「为什么 SyncLane 要用微任务而不是直接渲染?」
    因为「直接渲染」会让 setState 变成「立即、同步」的语义——那样「一次点击里的三次 setState」就会触发三次渲染,而不是批处理成一次。用 scheduleMicrotask(flushSyncCallbacks) 的含义是「等当前同步代码跑完之后再统一渲染」——这正是批处理的实现基础(也解释了「为什么事件处理里读不到新值」,见 12.2)。

  4. 「哪些更新不可以中断?」
    performConcurrentWorkOnRoot 里的判断是:shouldTimeSlice = !includesBlockingLane(lanes) && !includesExpiredLane(root, lanes) && !didTimeout。所以三种情况走 renderRootSync(不可中断):① 包含阻塞车道(SyncLane、InputContinuousLane——即点击、输入这类离散/连续事件);② 车道已过期(低优先级任务等太久 → 必须强制做完,避免饿死);③ Scheduler 告知 didTimeout。其余情况(如 DefaultLane、TransitionLanes)才走可中断的并发渲染。

  5. 「root.current = finishedWork 为什么在 mutation 之后、layout 之前?」
    因为这两个阶段对「树」的视角不同:mutation 阶段正在把「新树的 DOM」写进文档,此时读取操作(如 getSnapshotBeforeUpdate、以及内部依赖 current 的逻辑)仍然应该看到旧树;DOM 变更完成之后再切换 current,随后的 layout 阶段(赋 ref、useLayoutEffect)才看到新树。这个「切换时机」是「双缓存 + 副作用顺序」的关键衔接点。

  6. 「低优先级任务会不会被『饿死』?」
    不会——因为每个更新都有过期时间(expirationTime,由 priorityLevel 决定,如 userBlocking: 250ms、normal: 5000ms)。一旦过期,React 会在 includesExpiredLane() 里检测到并强制同步执行(走 renderRootSync,不再让出)。所以低优先级更新「可能很晚」但一定会执行。

  7. 「render 阶段为什么必须纯?从源码角度看。」
    因为 workLoopConcurrent 可能「在任意一个 performUnitOfWork 之后让出」——让出后,如果有更高优先级的更新插进来,React 会「丢弃当前 workInProgress 的部分成果并重新开始渲染」。也就是说 render 阶段的执行次数是不确定的(可能 0 次、1 次、多次)。所以任何副作用(改外部变量、发请求、读写 DOM)都会重复执行或产生不一致。这也是 StrictMode 双调用的理论基础(见 8.5)。

  8. 「fiber.updateQueue 为什么用环形链表?」
    两个原因:① O(1) 追加——维护一个 shared.pending 指针,新更新插到「pending 的末尾」,不需要遍历;② 批量消费——渲染时把 pending 切成一段独立链表(pending → pending.next 断开),消费完再对接回去。这样「渲染期间新来的更新」不会影响「本次正在消费的批次」,保证「一次渲染处理一个确定的更新集合」(这也与「优先级车道」配合,让高优先级更新能先被处理)。

一句话速记

链路形状:updateContainer(requestEventTime + requestUpdateLane + createUpdate + enqueueUpdate(环形链表))→ scheduleUpdateOnFiber(markRootUpdated + ensureRootIsScheduled)→ 分路(SyncLane 走 **微任务 flushSyncCallbacks,其它走 lanesToEventPriority → scheduleCallback)→ Scheduler(MessageChannel + 5ms 时间片 + shouldYieldToHost) → performConcurrentWorkOnRoot(shouldTimeSlice 判断阻塞车道/过期/超时)→ workLoopConcurrent(while (workInProgress && !shouldYield()) performUnitOfWork,beginWork 递 / completeWork 归,可中断)→ commitRootImpl(flushPassiveEffects → before mutation → mutation(含 useInsertionEffect)→ root.current = finishedWork → layout(useLayoutEffect)→ 调度 passive);三个关键结论:① SyncLane 用微任务是为了批处理、② 阻塞车道 / 过期车道 / didTimeout 不可中断、③ 低优先级有 expirationTime 所以不会被饿死;Scheduler 用 MessageChannel 而非 requestIdleCallback(兼容性 + 频率限制 + 空闲时间不可控);render 可能被重放/丢弃 → 必须纯**。

赞(0) 打赏
未经允许不得转载:陌上寒 » 4.17 源码走读:从 `createRoot` 到 `commitRoot`

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫