【React原理 - 任务调度和时间分片详解】

概述

在React15的时候,React使用的是从根节点往下递归的方式同步创建虚拟Dom,由于递归具有同步不可中断的特性,所以当执行长任务时(通常以60帧为标准,即16.6ms)就会长时间占用主线程长时间无响应,导致页面卡顿,对于交互及其不友好。所以React16中新增了fiber架构和scheduler调度(之前只有渲染器Renderer、协调器Reconciler),在该版本中新增了时间分片逻辑,将一个长任务切分为多个小任务,并在浏览器空闲时,根据优先级执行。其中时间分片的前提就是中断和恢复。本文以[email protected]来解释React中的时间分片以及中断和恢复原理。

调度器Scheduler

在了解时间分片之前,先了解下调度器是什么?

所谓Scheduler,如名所示就是用于任务调度,判断在什么时候执行任务避免长时间阻塞主线程的一个工具包。React官方将其设置为独立的包,不仅仅用于React,当其他项目需要对长任务进行优化调度时都可以使用该包,所以其命名为单独的scheduler,而不是react-scheduler

其主要有两个特性:

  • 优先级,根据优先级高低决定执行哪个任务,优先执行高优先级任务。
  • 时间分片,其内部设置了5ms的执行时间,当任务执行超过设置的时间则会中断执行,并将主线程交回浏览器,避免长时间无响应导致卡顿,在浏览器空闲时再通过恢复来继续执行中断的任务。

针对上面的两个特性,我们来一一解释:

优先级

在React内部优先级有三种:Event优先级Lane优先级Scheduler优先级

Event优先级:事件优先级,React会将原生事件封装成合成事件时,会根据事件类型携带不同的事件优先级。Event优先级定义时就映射了Lane优先级,即使用Lane优先级来进行的赋值。

javascript 复制代码
// react/packages/react-reconciler/src/ReactEventPriorities.js
export const NoEventPriority: EventPriority = NoLane; // 无优先级
export const DiscreteEventPriority: EventPriority = SyncLane; // 离散优先级
export const ContinuousEventPriority: EventPriority = InputContinuousLane; // 连续输入优先级
export const DefaultEventPriority: EventPriority = DefaultLane; // 默认优先级
export const IdleEventPriority: EventPriority = IdleLane; // 空闲优先级

Lane优先级 :车道优先级,位于react-reconciler协调器中,用于规定更新任务的执行顺序。

为什么是车道优先级呢? 可以看定义该优先级是使用二进制定义的,一方面二进制运算性能是很好的,其次React采用的是位优先级,不同的位表示具有该优先级,然后通过位运算能快速获取优先级。然后看后面的二进制定义,是不是像车道的样子,所以命名为车道优先级也表示其是使用二进制定义的。

javascript 复制代码
// react/packages/react-reconciler/src/ReactFiberLane.js
export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

export const SyncHydrationLane: Lane = /*               */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000010;
export const SyncLaneIndex: number = 1;

export const InputContinuousHydrationLane: Lane = /*    */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000001000;

export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /*          

Scheduler优先级 :调度优先级,位于scheduler中,用于制定更新任务调度的执行顺序。

javascript 复制代码
// react/packages/scheduler/src/SchedulerPriorities.js
export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;

// TODO: Use symbols?
export const NoPriority = 0; // 无优先级
export const ImmediatePriority = 1; // 离散事件,点击、keydown、input这种单个事件
export const UserBlockingPriority = 2; // 连续输入优先级,滚动、拖拽等连续事件
export const NormalPriority = 3; // 普通优先级(默认优先级)
export const LowPriority = 4; // 低优先级
export const IdlePriority = 5; // 空闲优先级

并且每种调度优先级都有自己的一个timeout,在unstable_scheduleCallback创建任务时会根据这个时延来设置expirationTime,并进一步设置sortIndex。这里只是介绍下,知道有这个东西,下面会详细说明。

javascript 复制代码
// react/packages/scheduler/src/SchedulerFeatureFlags.js
export const enableSchedulerDebugging = false;
export const enableProfiling = false;
export const frameYieldMs = 5;

export const userBlockingPriorityTimeout = 250;
export const normalPriorityTimeout = 5000;
export const lowPriorityTimeout = 10000;

转换体系 :Scheduler是独立的一个包,所以自己内部维护了一个优先级(Scheduler优先级),所以需要进行优先级转换:Lane优先级 -> Event优先级 -> Scheduler优先级进行Lane优先级和Scheduler优先级的映射。比如当我们点击按钮触发onclick事件时,由于合成事件携带了Event优先级,并且该Event优先级是Lane优先级的映射,然后在协调器中创建任务调度时,会根据该Event优先级转换为Scheduler优先级。如下所示:在下面代码中创建调度任务时会执行scheduleTaskForRootDuringMicrotask函数,并在其中会根据lane优先级获取Event优先级,然后转换为Scheduler优先级

javascript 复制代码
// react/packages/react-reconciler/src/ReactEventPriorities.js
export function lanesToEventPriority(lanes: Lanes): EventPriority {
  const lane = getHighestPriorityLane(lanes);
  if (!isHigherEventPriority(DiscreteEventPriority, lane)) {
    return DiscreteEventPriority;
  }
  if (!isHigherEventPriority(ContinuousEventPriority, lane)) {
    return ContinuousEventPriority;
  }
  if (includesNonIdleWork(lane)) {
    return DefaultEventPriority;
  }
  return IdleEventPriority;
}

// react/packages/react-reconciler/src/ReactFiberRootScheduler.js
import {
  ImmediatePriority as ImmediateSchedulerPriority,
  UserBlockingPriority as UserBlockingSchedulerPriority,
  NormalPriority as NormalSchedulerPriority,
  IdlePriority as IdleSchedulerPriority,
  cancelCallback as Scheduler_cancelCallback,
  scheduleCallback as Scheduler_scheduleCallback,
  now,
} from './Scheduler';

function scheduleTaskForRootDuringMicrotask(
  root: FiberRoot,
  currentTime: number,
): Lane {
// ...
	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;
    }
// ...
}

从上面的源码来看对应关系如下:

Event Priority Lane Priority Scheduler Priority 说明
NoEventPriority NoLane NormalSchedulerPriority 无优先级
DiscreteEventPriority SyncLane ImmediateSchedulerPriority 离散事件优先级(如点击)
ContinuousEventPriority InputContinuousLane UserBlockingSchedulerPriority 连续输入事件优先级(如拖拽)
DefaultEventPriority DefaultLane NormalSchedulerPriority 默认优先级
IdleEventPriority IdleLane IdleSchedulerPriority 空闲优先级

时间分片

所谓时间分片就是将一个长任务拆分为多个小任务,避免执行长任务导致主线程无法响应而页面卡顿问题。拆分的小任务会在浏览器空闲时执行,并且会定时将控制权还回浏览器,React中默认每个小任务执行时间为5ms,所以在每一帧可能会执行多次,一旦主线程没有其他任务就会执行该小任务(中断/恢复),并不像requestAnimationFrame一样,每一帧只会执行一次。时间分片逻辑主要在shouldYieldToHost/unstable_shouldYield函数中

unstable_shouldYieldshouldYieldToHost是同一个函数,只是导出别名。export {shouldYieldToHost as unstable_shouldYield }

javascript 复制代码
// react/packages/scheduler/src/SchedulerFeatureFlags.js
export const frameYieldMs = 5;
// react/packages/scheduler/src/forks/Scheduler.js
let frameInterval = frameYieldMs;
let startTime = -1;

function shouldYieldToHost(): boolean {
  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;
  }
  // Yield now.
  return true;
}

export {
  ...
  shouldYieldToHost as unstable_shouldYield,
  ...
};

从代码也能看出,该函数shouldYieldToHost返回一个布尔值,表示当前是否需要将控制权还回浏览器,其中根据已经执行任务时间是否超过设置的定时帧即timeElapsed < frameInterval来返回控制权,避免长时间阻塞。并且该函数会定时执行,以确保及时返回控制权。其中从下面的React工作循环示意图中,该函数会在其两大循环中都会执行,任务调度循环中调度任务之前以及fiber构造循环之前都会使用该函数进行判断,具体逻辑下面细说。

任务创建和调度

到这里,我们已经对Scheduler的主要特性有所了解了。我们都知道当我们通过setState触发状态更新后,会发起创建一个更新任务并创建调度任务并等待Scheduler调度,然后在Reconciler中执行fiber构造。所以下面开始介绍Scheduler中的任务创建和调度,进而细说介绍其中的时间分片技术。

先看下网上很火的流程图:

一步一步来,先看任务注册和调度流程。即状态更新 -> dispatchState -> scheduleUpdateOnFiber -> ensureRootIsScheduled -> scheduleCallback -> unstable_scheduleCallback -> requestHostCallback(创建任务、timerQueue、taskQueue) -> schedulePerformWorkUntilDeadline -> performWorkUntilDeadline(通过MessageChannel) -> flushWork -> workLoop -> 后续就是执行调度任务回调,即执行performConcurrentWorkOnRoot函数

状态更新操作通常指setStateforceUpdate这种状态更新重新渲染,并且要知道不是所有的更新都会进入Scheduler调度,通常将可以延迟的长任务、低优先级任务才会进入调度,否则都是同步执行的(优先级默认为SyncLane)。长任务startTransitionuseDeferredValue低优先级通常这些任务才会通过ensureRootIsScheduled进入scheduleCallback调度,因为对于这种优先级不是很高的任务会降低其优先级,然后进入等待调度逻辑。其中关于这两个React18新加入的api可以查看这篇文章: 【React Hooks原理 - useDeferredValue】、【React Hooks原理 - useTransition】其本质就是降低优先级,以达延迟执行的目的,本文不再赘述。

上面主要列举了从状态更新到更新任务调度执行的整个函数调用,先总体有个眼熟。下面来看代码,并会根据demo结合浏览器bugger来逐一介绍,(其中主要介绍的是重要函数的重要逻辑,不会每行介绍),其中demo运行在[email protected]的环境下。

demo示例:

javascript 复制代码
import React, { useState, startTransition } from "react";

function SearchComponent() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  function handleChange(event) {
    setQuery(event.target.value);
    startTransition(() => {
      // 模拟搜索过程
      const filteredResults = performSearch(event.target.value);
      setResults(filteredResults);
    });
  }

  return (
    <div>
      <input type="text" value={query} onChange={handleChange} />
      <ul>
        {results.map((result, index) => (
          <li key={index}>{result}</li>
        ))}
      </ul>
    </div>
  );
}

function performSearch(query) {
  // 模拟一个复杂的搜索算法
  return Array(10000)
    .fill(0)
    .map((_, index) => `Result ${index} for ${query}`);
}

export default SearchComponent;

1、触发状态更新

通过点击异步按钮,触发setState进入dispatchState逻辑。

javascript 复制代码
function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
): void {

  const lane = requestUpdateLane(fiber);

  const update: Update<S, A> = {
    lane,
    revertLane: NoLane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  const alternate = fiber.alternate;
  // 判断当前更新队列中是否有其他更新任务
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher = null;
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          update.hasEagerState = true;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return;
          }
        }
      }
    }

    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      scheduleUpdateOnFiber(root, fiber, lane);
      entangleTransitionUpdate(root, queue, lane);
    }
}

该函数主要逻辑如下:

  • requestUpdateLane获取本次更新任务的Lane优先级
  • 当前没有其他更新任务会进入预计算逻辑,即获取更新后结果eagerState然后判断值是否变化,以进行enqueueConcurrentHookUpdateAndEagerlyBailout跳过本次更新。由于我们示例是在第一个setState断点的,所以第一次会进入到当前逻辑,后面的setState不会进入该逻辑。
  • enqueueConcurrentHookUpdate执行该函数将本次更新任务添加到更新队列中,这是React18对于异步批量处理的优化,详细可看这篇文章: 【React Hooks - useState状态批量更新原理
  • 然后通过scheduleUpdateOnFiber开始进入调度器调度阶段。

2、进入Scheduler调度

scheduleUpdateOnFiber中主要就是标记计算优先级、标记更新节点等。其中最重要的是调用ensureRootIsScheduled开始进入调度。

javascript 复制代码
export function ensureRootIsScheduled(root: FiberRoot): void {
  // Schedule a new callback.
  let newCallbackNode;
  if (newCallbackPriority === SyncLane) {
    // 处理同步任务
  } else {
    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),
    );
  }
}

ensureRootIsScheduled函数中会通过newCallbackPriority === SyncLane进行优先级判断,那些任务需要通过Scheduler调度,那些直接同步执行。从上面优先级我们知道对于 长任务、startTransition、useDeferredValue、低优先级通常这些任务才会通过ensureRootIsScheduled进入scheduleCallback调度。在通过scheduleCallback申请调度之前,会先进行优先级转换即:Lane -> Event -> Scheduler优先级,并将转换后的Scheduler优先级和performConcurrentWorkOnRoot回调传入调度器中,该回调就是在调度器中频繁出现的callback。

3、创建调度任务

上面两部都是在协调器React-Concilder中进行的,通过scheduleCallback进入调度器中即Scheduler包的unstable_scheduleCallback函数。

javascript 复制代码
// packages/scheduler/src/forks/Scheduler.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;
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }

  var expirationTime = startTime + timeout;

  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  if (startTime > currentTime) {
    // This is a delayed task.
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // Schedule a host callback, if needed. If we're already performing work,
    // wait until the next time we yield.
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

可以将unstable_scheduleCallback函数拆分为4步:

  • 根据传入的可选参数option计算startTime
  • 根据Scheduler优先级计算expirationTime,上面说过每个Scheduler优先级都对应了一个timeout时延
  • 创建调度任务,newTask
  • 根据开始时间和当前时间判断当前调度任务是否需要执行。

其中对于第4步,我们要知道其中的两个队列:timerQueuetaskQueue。其中两者都是使用小顶堆的结构存储调度任务,因为小顶堆具有父节点值比其子节点值小的特性,再结合优先级值越小优先级越高的特性,所以在该Queue中越前面的任务优先级越高就需要优先执行。

timerQueue

timerQueue保存的就是还未到执行时间的任务,其内部会通过requestHostTimeout调用setTimeout延时调用handleTimeout。主要涉及两个函数handleTimeoutadvanceTimers

javascript 复制代码
function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;
  advanceTimers(currentTime);

  if (!isHostCallbackScheduled) {
    if (peek(taskQueue) !== null) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

handleTimeout逻辑如下:

  • 调用advanceTimers函数,根据当前时间和任务开始时间判断,当到执行该任务时,会将其从timerQueue中移到taskQueue进行执行
  • 判断taskQueue是否有需要执行的任务,有则调用requestHostCallback发起调度,没有则递归调用requestHostTimeout将timerQueue加入到taskQueue中。
javascript 复制代码
function advanceTimers(currentTime) {
  // Check for tasks that are no longer delayed and add them to the queue.
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      // Timer was cancelled.
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // Timer fired. Transfer to the task queue.
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // Remaining timers are pending.
      return;
    }
    timer = peek(timerQueue);
  }
}

其中需要注意调度任务的sortIndex属性,由于taskQueue/timerQueue内部使用小顶堆实现,该值表示其所在的位置,优先级越高过期时间越早该值就会越小,该任务所在位置就越在小顶堆上方,就越先被执行。

taskQueue

保存已经到达执行时间的调度任务。当当前没有正在调度的任务时即!isHostCallbackScheduled,就会执行requestHostCallback函数调度该任务。

不管有没有到执行时间,最后都是通过requestHostCallback函数进行调度。无非当没到时间时,会先保存在timerQueue中等到时间之后在移动到taskQueue中执行

javascript 复制代码
function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

这里无非就是将传入的callback即传入的flushWork函数绑定给scheduledHostCallback,然后当前没有任务执行时调用schedulePerformWorkUntilDeadline

flushWork:

javascript 复制代码
function flushWork(hasTimeRemaining, initialTime) {
	...
	return workLoop(hasTimeRemaining, initialTime);
	... 
}

flushWork函数主要就是调用了workLoop来进行React两大工作循环的调度循环(While循环)。而workLoop函数可以拆分为三部分来看:

  • 调用advanceTimers将到达执行时间的任务移动到taskQueue,等待调度
  • 通过While进行调度循环执行回调
  • 当然任务是否执行完成,如果执行完成则从timerQueue中获取任务,否则继续执行。
javascript 复制代码
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);

  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // This currentTask hasn't expired, and we've reached the deadline.
      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);
  }
  
  // Return whether there's additional work
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

其中第一、三部分比较简单,这里介绍下第二部分:While调度循环

  • 首先就是根据时间和shouldYieldToHost判断是否需要跳过当前任务currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())
  • 判断当前callback是否是函数,不是函数表示当前任务执行完成,则将其从taskQueue中去除。如果被高优先级任务中断,则该callback是函数并且不会将该任务从taskQueue中删除,下一次仍然继续执行。
  • 执行callback即performConcurrentWorkOnRoot函数,根据返回值是否为function来判断当前任务是否执行完成

这里要知道workLoop返回的布尔值就表示当前任务是否被执行完成,即下面说的hasMoreWork变量。

schedulePerformWorkUntilDeadline:

javascript 复制代码
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
   schedulePerformWorkUntilDeadline = () => {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

从上面能知schedulePerformWorkUntilDeadline就是根据当前不同的环境以不同的方式来执行performWorkUntilDeadline函数。

  • localSetImmediate:在node环境或者低版本IE中通过setImmediate来触发
  • MessageChannel:在浏览器端通过MessageChannel触发
  • localSetTimeout: 使用setTimeout兜底兼容。
为什么使用MessageChannel?

在浏览器的事件循环EventLoop中普遍认为存在两个队列: 微任务队列、宏任务队列。由于浏览器在每一帧(60FPS下就是16.6ms)会做很多的工作比如Js执行、布局、绘制渲染等。而浏览器会在保证交互流畅的前提下在一帧中会先执行一次宏任务然后执行其后所有的微任务。如果我们使用微任务进行调度则不能随时将控制权交会给浏览器进行高优先级处理,所以React使用宏任务进行任务调度。

那有这么多宏任务: MessageChannel、setTimeout、requestIdleCallback为什么React最终会选择MessageChannel呢?

简单说就是requestIdleCallback存在兼容性问题、setTimeout存在最低4ms延时,所以React在多次对比之后选择了MessageChannel并使用setTimeout兜底的方案。详细介绍MessageChannel可查看这篇文章: 【React架构 - Scheduler中的MessageChannel

下面是浏览器Event Loop、Scheduler workLoop、Reconciler workLoop三者的关系:(图片来源网络)

所以每次任务调度,Scheduler都会通过MessageChannel向浏览器发生一个宏任务,然后由浏览器根据当前使用情况判断在当前帧或者下一帧执行该任务,进行调度更新

回到本文,从上面代码能看出通过MessageChannel发送调度任务后最终执行performWorkUntilDeadline函数来执行该调度任务

javascript 复制代码
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;
};

其中主要看三个属性: scheduledHostCallbackhasMoreWorkisMessageLoopRunning

  • scheduledHostCallback就是上面提到的performConcurrentWorkOnRoot回调,该回调在Reconciler中开始进行fiber构造。
  • hasMoreWork表示当前任务是否执行完成,如果被高优先级任务中断则会返回true,在finally中会根据该字段判断是否在继续执行。
  • isMessageLoopRunning最后执行完成之后设置该变量为false,表示当前没有任务执行。

至此我们就介绍完了Scheduler的主要流程。

总结

从上面的介绍我们知道,在React中并不是所有的状态更新都会经过调度,而是将长任务、低优先级、startTransitionuseDeferredValue这种长耗时并且优先级不高的任务通过shouldYieldToHost函数进行时间切片,并更新taskQueue来调度执行任务。

参考文章

万字长文 - 彻底理解react中任务调度和时间分片

相关推荐
腾讯TNTWeb前端团队6 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom11 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom11 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom11 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom11 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom11 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试