React 源码 - Scheduler

38 阅读3分钟

1、时间切片

  • Scheduler时间切片功能是通过task(宏任务)实现的
  • Scheduler将需要被执行的回调函数作为MessageChannel的回调执行
    • 支持 :channel = new MessageChannel();
    • 不支持:使用 setTimeout
  • render阶段通过Scheduler提供的shouldYield方法判断是否需要中断遍历
// js执行机制
// requestAnimationFrame : 用于封装 MessageChannel 方法
t宏任务 --> 微任务 --> requestAnimationFrame --> 重排/重绘 --> requestIdleCallback
function workLoopConcurrent() {
  // 1、循环便利,是否有时间执行当前任务
  // 2、如果是同步任务,则没有 shouldYield 判断条件
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
import {enableIsInputPending} from '../SchedulerFeatureFlags';
export let requestHostCallback;
export let cancelHostCallback;
export let requestHostTimeout;
export let cancelHostTimeout;
export let shouldYieldToHost;
export let requestPaint;
export let getCurrentTime;
export let forceFrameRate;

if ( typeof window === 'undefined' ||  typeof MessageChannel !== 'function') {
  // 不支持 MessageChannel 使用 setTimeout 处理切片任务
  ...
  
} else {
  const performance = window.performance;
  const Date = window.Date;
  const setTimeout = window.setTimeout;
  const clearTimeout = window.clearTimeout;

  // 当前时间戳
  if (
    typeof performance === 'object' &&
    typeof performance.now === 'function'
  ) {
    getCurrentTime = () => performance.now();
  } else {
    const initialTime = Date.now();
    getCurrentTime = () => Date.now() - initialTime;
  }

  let isMessageLoopRunning = false;
  let scheduledHostCallback = null;
  let taskTimeoutID = -1;

 
  // 定义最小时间间隔
  let yieldInterval = 5;
  let deadline = 0;
  const maxYieldInterval = 300;
  let needsPaint = false;

  if (
    enableIsInputPending &&
    navigator !== undefined &&
    navigator.scheduling !== undefined &&
    navigator.scheduling.isInputPending !== undefined
  ) {
    const scheduling = navigator.scheduling;
    shouldYieldToHost = function() {
      const currentTime = getCurrentTime();
      if (currentTime >= deadline) {
        if (needsPaint || scheduling.isInputPending()) {
          return true;
        }
        return currentTime >= maxYieldInterval;
      } else {
        return false;
      }
    };

    requestPaint = function() {
      needsPaint = true;
    };
  } else {

    shouldYieldToHost = function() {
      return getCurrentTime() >= deadline;
    };
    requestPaint = function() {};
  }

  // 根据当前刷新频率,动态修改 yieldInterval
  forceFrameRate = function(fps) {
    if (fps < 0 || fps > 125)  return;
    if (fps > 0) {
      yieldInterval = Math.floor(1000 / fps);
    } else {
      yieldInterval = 5;
    }
  };

  const performWorkUntilDeadline = () => {
    if (scheduledHostCallback !== null) {
      const currentTime = getCurrentTime();
      deadline = currentTime + yieldInterval;
      const hasTimeRemaining = true;
      try {
        const hasMoreWork = scheduledHostCallback(
          hasTimeRemaining,
          currentTime,
        );
        if (!hasMoreWork) {
          isMessageLoopRunning = false;
          scheduledHostCallback = null;
        } else {
          port.postMessage(null);
        }
      } catch (error) {
        port.postMessage(null);
        throw error;
      }
    } else {
      isMessageLoopRunning = false;
    }
    needsPaint = false;
  };


  // 创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;

  requestHostCallback = function(callback) {
    scheduledHostCallback = callback;
    if (!isMessageLoopRunning) {
      isMessageLoopRunning = true;
      port.postMessage(null);
    }
  };

  cancelHostCallback = function() {
    scheduledHostCallback = null;
  };

  requestHostTimeout = function(callback, ms) {
    taskTimeoutID = setTimeout(() => {
      callback(getCurrentTime());
    }, ms);
  };

  cancelHostTimeout = function() {
    clearTimeout(taskTimeoutID);
    taskTimeoutID = -1;
  };
}

2、优先级调度

  • Scheduler是独立于React的包,他的优先级也是独立于React优先级
  • Scheduler对外暴露了一个方法 unstable_runWithPriority
// 同步任务
function commitRoot(root) {
  // 获取当前任务的优先级
  const renderPriorityLevel = getCurrentPriorityLevel();
  
  // schedule 的优先级介入 react 的优先级中
  runWithPriority(
    ImmediateSchedulerPriority,
    commitRootImpl.bind(null, root, renderPriorityLevel),
  );
  return null;
}
// 异步任务优先级,schedule 接入 react 优先级
function unstable_runWithPriority(priorityLevel, eventHandler) {
  // 传入的 5 种 react 优先级
  switch (priorityLevel) {
    case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:
      break;
    default:
      priorityLevel = NormalPriority;
  }

  // 保存当前优先级
  var previousPriorityLevel = currentPriorityLevel;
  // 替换为传入优先级
  currentPriorityLevel = priorityLevel;

  try {
    // 回调函数,任务执行过程中获取的优先级都是传入的 priorityLevel
    return eventHandler();
  } finally {
    // 还原为之前的优先级
    currentPriorityLevel = previousPriorityLevel;
  }
}

3、优先级的意义

  • Scheduler对外暴露最重要的方法便是unstable_scheduleCallback
  • 用于以某个优先级注册回调函数
  • commit阶段的beforeMutation阶段会调度useEffect的回调
  • 不同优先级意味着不同时长的任务过期时间
// commit阶段的beforeMutation阶段会调度useEffect的回调
if (!rootDoesHavePassiveEffects) {
  rootDoesHavePassiveEffects = true;
  scheduleCallback(NormalSchedulerPriority, () => {

    // effect的回调
    flushPassiveEffects();
    return null;
  });
}

// react 调度优先级
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;
  }

  var expirationTime = startTime + timeout;
  // 创建一个新的任务
  var newTask = ...
  ... 参考下面 ⬇️

  return newTask;
}

4、优先级排序

  • timerQueue:未过期任务
  • taskQueue:已过期任务
  • 每当有新的未就绪的任务被注册,我们将其插入timerQueue并根据开始时间重新排列timerQueue中任务的顺序。当timerQueue中有任务就绪,即startTime <= currentTime,我们将其取出并加入taskQueue。取出taskQueue中最早过期的任务并执行他。为了能在O(1)复杂度找到两个队列中时间最早的那个任务,Scheduler使用小顶堆 (opens new window)实现了优先级队列
var expirationTime = startTime + timeout;

// 创建一个新的任务
var newTask = {
  id: taskIdCounter++,
  callback,
  priorityLevel,
  startTime,
  expirationTime,
  sortIndex: -1,
};
if (enableProfiling) {
  newTask.isQueued = false;
}

// 没过期
if (startTime > currentTime) {
  newTask.sortIndex = startTime;
  // timerQueue 中 插入新任务,小顶堆
  push(timerQueue, newTask);

  // 没有过期任务,且 newTask 为过期时间最近的任务
  if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
    if (isHostTimeoutScheduled) {
      cancelHostTimeout();
    } else {
      isHostTimeoutScheduled = true;
    }

    // 实时检查 timerQueue 队列中,任务是否过期
    requestHostTimeout(handleTimeout, startTime - currentTime);
  }
} else {
  // 已过期
  newTask.sortIndex = expirationTime;

  // taskQueue 中 插入新任务,小顶堆
  push(taskQueue, newTask);
  if (enableProfiling) {
    markTaskStart(newTask, currentTime);
    newTask.isQueued = true;
  }
  if (!isHostCallbackScheduled && !isPerformingWork) {
    isHostCallbackScheduled = true;
    // 执行过期的任务
    requestHostCallback(flushWork);
  }
}