react 调度器学习笔记

185 阅读4分钟

经常听说React的Scheduler是通过 requestIdleCallback 来实现的,但是全局搜索 react 却发现并没有实际调用 requestIdleCallback 的地方。。

深入读源码,发现实际使用的是 MessageChannelsetTimeout 。。。
react 的 整个执行过程就好像是一个浏览器的生命循环,开始之后,就会不停地循环,直到当前的任务队列运行结束
react的任务队列是一个小顶堆,任务优先级最高的在最上面,在执行 workLoop 的时候,就执行出栈

思考

为什么主要使用 MessageChannel,而不是宣传的 requestIdleCallback,也不是更为大家所知的 timeout 或者 Promise

  1. requestIdleCallback 的兼容性不是很理想
  2. setTimeout 在第一次执行之后,最低的执行间隔为 4-6 ms,这对于 5ms 为执行周期的 react 是不能忍受的,而且 MessageChannel 的执行优先级高于 setTimeout
  3. Promise 是一个 微任务,一直执行的话,会导致页面无法操作

源码分析

1. 导入

react 里调度器的代码导入、导出
packages/react-reconciler/src/SchedulerWithReactIntegration.new.js#25

import * as Scheduler from 'scheduler';

const {
  unstable_scheduleCallback: Scheduler_scheduleCallback,
  unstable_cancelCallback: Scheduler_cancelCallback,
  unstable_shouldYield: Scheduler_shouldYield,
  unstable_requestPaint: Scheduler_requestPaint,
  unstable_now: Scheduler_now,
  unstable_getCurrentPriorityLevel: Scheduler_getCurrentPriorityLevel,
  unstable_ImmediatePriority: Scheduler_ImmediatePriority,
  unstable_UserBlockingPriority: Scheduler_UserBlockingPriority,
  unstable_NormalPriority: Scheduler_NormalPriority,
  unstable_LowPriority: Scheduler_LowPriority,
  unstable_IdlePriority: Scheduler_IdlePriority,
} = Scheduler;

2. 开启 react 的 Scheduler 进行执行

packages/scheduler/src/forks/SchedulerDOM.js#312

  1. 判断任务优先级
  2. 计算任务过期时间
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;
}

packages/scheduler/src/forks/SchedulerDOM.js#586

function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

3. 给 schedulePerformWorkUntilDeadline 赋值

packages/scheduler/src/forks/SchedulerDOM.js#554 \

  1. 在正常的浏览器环境下,就是 port.postMessage(null); 执行了之后,就会触发 MessageChannelonmessage
  2. 就会调用 performWorkUntilDeadline
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  // DOM and Worker environments.
  // We prefer MessageChannel because of the 4ms setTimeout clamping.
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  // We should only fallback here in non-browser environments.
  schedulePerformWorkUntilDeadline = () => {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

4. 开启时间循环 performWorkUntilDeadline

packages/scheduler/src/forks/SchedulerDOM.js#519
这里就做了一个回调,继续调用schedulePerformWorkUntilDeadline
就是模拟了 requestIdleCallback 函数,在浏览器空闲的时候,继续执行任务

const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // Keep track of the start time so we can measure how long the main thread
    // has been blocked.
    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;
  }
  // Yielding to the browser will give it a chance to paint, so we can
  // reset this.
  needsPaint = false;
};
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield

  if (workInProgressIsSuspended) {
    // The current work-in-progress was already attempted. We need to unwind
    // it before we continue the normal work loop.
    const thrownValue = workInProgressThrownValue;
    workInProgressIsSuspended = false;
    workInProgressThrownValue = null;
    if (workInProgress !== null) {
      resumeSuspendedUnitOfWork(workInProgress, thrownValue);
    }
  }

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

5. 执行任务出栈

packages/scheduler/src/forks/SchedulerDOM.js#147 \

  1. requestHostCallback 执行的回调函数就是 flushWork
  2. flushWork 确认当前还在执行时间内,调用 workLoop
  3. workLoop 执行任务从小顶堆中弹出,并开始执行
function flushWork(hasTimeRemaining, initialTime) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    if (enableProfiling) {
      try {
        return workLoop(hasTimeRemaining, initialTime);
      } catch (error) {
        if (currentTask !== null) {
          const currentTime = getCurrentTime();
          markTaskErrored(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        throw error;
      }
    } else {
      // No catch in prod code path.
      return workLoop(hasTimeRemaining, initialTime);
    }
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
    if (enableProfiling) {
      const currentTime = getCurrentTime();
      markSchedulerSuspended(currentTime);
    }
  }
}

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') {
        // If a continuation is returned, immediately yield to the main thread
        // regardless of how much time is left in the current time slice.
        currentTask.callback = continuationCallback;
        if (enableProfiling) {
          markTaskYield(currentTask, currentTime);
        }
        advanceTimers(currentTime);
        return true;
      } 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;
  }
}

6. shouldYieldToHost 是否打断某个函数的执行

packages/react-reconciler/src/ReactFiberWorkLoop.new.js#2016 通过 shouldYield 来判断是否打断当前任务

/** @noinline */
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield

  if (workInProgressIsSuspended) {
    const thrownValue = workInProgressThrownValue;
    workInProgressIsSuspended = false;
    workInProgressThrownValue = null;
    if (workInProgress !== null) {
      resumeSuspendedUnitOfWork(workInProgress, thrownValue);
    }
  }

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

packages/scheduler/src/forks/Scheduler.js#444

  1. getCurrentTime 使用了 performance.now || Date.now
    优先使用 performance.now,这个页面打开开始执行,不受本地时间修改而改动,而且,精确到了微秒
  2. 当执行时间超过 5ms 时,打断当前任务,交出执行权
let getCurrentTime;
const hasPerformanceNow =
  typeof performance === 'object' && typeof performance.now === 'function';

if (hasPerformanceNow) {
  const localPerformance = performance;
  getCurrentTime = () => localPerformance.now();
} else {
  const localDate = Date;
  const initialTime = localDate.now();
  getCurrentTime = () => localDate.now() - initialTime;
}

function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;
  // frameYieldMs 是写死的 5 ms
  if (timeElapsed < frameInterval) {
    return false;
  }
  if (enableIsInputPending) {
    if (needsPaint) {
      return true;
    }
    if (timeElapsed < continuousInputInterval) {
      if (isInputPending !== null) {
        return isInputPending();
      }
    } else if (timeElapsed < maxInterval) {
      // Yield if there's either a pending discrete or continuous input.
      if (isInputPending !== null) {
        return isInputPending(continuousOptions);
      }
    } else {
      return true;
    }
  }
  return true;
}