深入分析react scheduler

289 阅读6分钟

scheduler模块react为了实现fiber架构新增的一个模块,主要用来实现任务的拆分和调度执行。

scheduler是一个独立的模块,暴露了一些任务调度相关的方法,可以直接调用。

我们可以从npm仓库直接安装使用:

const scheduler = require('scheduler')

scheduler.unstable_scheduleCallback(scheduler.unstable_ImmediatePriority, () => {
    console.log(111)
})

scheduler.unstable_scheduleCallback(scheduler.unstable_NormalPriority, () => {
    console.log(222)
})

scheduleCallback可以认为是 scheduler 的入口方法,用于添加任务。这个方法主要有3个参数

  1. priorityLevel:任务的优先级
  2. callback:回调,任务的具体逻辑
  3. options:额外的参数,可以设置delay

scheduler 根据重要程度给任务分成了5个优先级

  • NoPriority = 0;
  • ImmediatePriority = 1;
  • UserBlockingPriority = 2;
  • NormalPriority = 3;
  • LowPriority = 4;
  • IdlePriority = 5;

除了 NoPriority表示最低的优先级外,剩余的5个数值越小优先级越高。

下面看一下scheduleCallback方法

function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime(); // 通过performance.now()获取当前时间

  // 判断 options 参数中是否有延迟字段( delay ),有的话把 currentTime 和 delay 相加
  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;
  }

  // 这里需要说一下延迟时间(delay)和超时时间(timeout)的区别,延迟时间是业务定义的必须执行等待的;
  // 而超时时间是根据任务优先级映射的时间,主要作用是给任务按优先级排序执行,只要没有高优先级的任务了,低优先级的任务也会立即执行。

  // 当前时间(currentTime) + 延迟时间(delay) + 超时时间(timeout) = 任务的过期时间
  var expirationTime = startTime + timeout;

  // 新建一个任务
  var newTask = {
    id: taskIdCounter++, // 递增的id
    callback,            // 业务逻辑 
    priorityLevel,       // 优先级 
    startTime,           // 任务开始执行时间 currentTime + delay
    expirationTime,      // 过期时间 currentTime + delay + timeout
    sortIndex: -1,       // 排序索引
  };

  // 所有profile相关的都是用于代码调试,可忽略
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  if (startTime > currentTime) { // 有延迟(delay)的情况
    // This is a delayed task.
    newTask.sortIndex = startTime; // 设置排序索引为开始时间,通过开始时间来给任务的执行排序
    push(timerQueue, newTask); // 把任务放入到timerQueue,timerQueue是用来存储有延迟(delay)的任务,当延迟时间到了时会把任务从timerQueue转移到taskQueue中
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) { // 如果没有可执行的任务,并且上面新创建的任务是下一次待执行的任务
    
      // 如果最近的一个任务需要延迟执行,则通过setTimeout方法,暂停delay时间后执行任务。主要用于性能优化,在没有任务时暂停代码运行
      // 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 { // 没有延迟(delay)的情况,说明当前肯定有任务需要执行
    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); // 在下一个 event loop 执行 flushWork 方法
    }
  }

  return newTask;
}

下面我们看一下 requestHostCallback 方法

function requestHostCallback(callback) {
  scheduledHostCallback = callback; // 把 flushWork 方法赋给 scheduledHostCallback
  if (!isMessageLoopRunning) { // 是否有任务正在执行
    isMessageLoopRunning = true;
    port.postMessage(null); // Scheduler 首选用 MessageChannel 调度下一个 event loop,在不支持 MessageChannel 的环境,或 nodejs 环境也 polyfill 了 setTimeout 的实现版本。主要的原因是 setTimout 方法自带了几毫秒的延时,在有大量的任务时会有比较大的性能问题
  }
}

具体的 MessageChannel 的应用

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline; // 当收到 postMessage 方法时会调用 performWorkUntilDeadline

每次 event loop 在 performWorkUntilDeadline 方法中处理

const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) { // scheduledHostCallback 表示 flushWork
    const currentTime = getCurrentTime(); // 获取当前时间
    // Yield after `yieldInterval` ms, regardless of where we are in the vsync
    // cycle. This means there's always time remaining at the beginning of
    // the message event.
    deadline = currentTime + yieldInterval; // yieldInterval 表示每个时间片可以执行任务的时间长度,默认5毫秒,可以通过 forceFrameRate 方法动态计算;在每个时间片内还会有多次 postMessage
    const hasTimeRemaining = true; // 是否还有剩余时间
    try {
      const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime); // 执行 flushWork 方法
      if (!hasMoreWork) { // 如果没有可执行的任务了
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      } else {
        // If there's more work, schedule the next message event at the end
        // of the preceding one.
        port.postMessage(null); // 如果还有可执行的任务 postMessage 递归调用 performWorkUntilDeadline 和 flushWork 方法
      }
    } catch (error) {
      // If a scheduler task throws, exit the current browser task so the
      // error can be observed.
      port.postMessage(null);
      throw error;
    }
  } else {
    isMessageLoopRunning = false; 
  }
  // Yielding to the browser will give it a chance to paint, so we can
  // reset this.
  needsPaint = false;
};

下面看一下 flushWork 方法

function flushWork(hasTimeRemaining, initialTime) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  // We'll need a host callback the next time work is scheduled.
  isHostCallbackScheduled = false; // 取消暂停调度
  if (isHostTimeoutScheduled) { // 把延迟暂停取消
    // We scheduled a timeout but it's no longer needed. Cancel it.
    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 {
    // 执行完成所有taskQueue中的任务
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel; 
    isPerformingWork = false; // 标识执行任务结束
    if (enableProfiling) {
      const currentTime = getCurrentTime();
      markSchedulerSuspended(currentTime);
    }
  }
}

workLoop 方法

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime; // 执行任务开始时间
  advanceTimers(currentTime); // 把不再有延迟的任务(delay时间到了)从 timerQueue 放入到 taskQueue
  currentTask = peek(taskQueue); // 从 taskQueue 按排序取下一个需要执行的任务
  while (
    currentTask !== null && // 任务不为空
    !(enableSchedulerDebugging && isSchedulerPaused) // 这2个参数用于debug
  ) {
    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; // 任务过期了
      markTaskRun(currentTask, currentTime); // 用于调试
      const continuationCallback = callback(didUserCallbackTimeout); // 执行任务, 任务有一个参数,表示任务是否过期
      currentTime = getCurrentTime(); // 获取当前时间
      if (typeof continuationCallback === 'function') { // 如果任务会返回一个函数
        currentTask.callback = continuationCallback; // 把返回的函数赋值给当前任务的回调
        markTaskYield(currentTask, currentTime); // 用于调试
      } else { // 否则,删除该次任务
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      advanceTimers(currentTime); // 再次执行: 把不再有延迟的任务(delay时间到了)从 timerQueue 放入到 taskQueue
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue); // 取出下一个任务
  }
  // Return whether there's additional work
  if (currentTask !== null) { // 如果存在下一个任务,结束调用,进入下一次 event loop
    return true;
  } else {
      // 如果不存在下一个任务,并且存在延迟的任务,暂停delay时间后再执行任务
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

最后一个没有仔细讲到的点是任务的排序,我们可以看一下 在 SchedulerMinHeap 文件中定义的几个方法

// 把任务放入到队列中,并执行 siftUp 方法
export function push(heap: Heap, node: Node): void {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}

// 取出第一个任务
export function peek(heap: Heap): Node | null {
  const first = heap[0];
  return first === undefined ? null : first;
}

// 返回第一个任务,把最后一个任务放到第一个位置,调用siftDown
export function pop(heap: Heap): Node | null {
  const first = heap[0];
  if (first !== undefined) {
    const last = heap.pop();
    if (last !== first) {
      heap[0] = last;
      siftDown(heap, last, 0);
    }
    return first;
  } else {
    return null;
  }
}

// siftUp 方法,push 后调用
function siftUp(heap, node, i) {
  let index = i; // i 是 push 后的任务的位置,也就是列表的最后一个位置
  while (true) {
    const parentIndex = (index - 1) >>> 1;// 把位置做位移操作,比如1 >>> 1 = 0, 2 >>> 1 = 1, , 4 >>> 1 = 2
    const parent = heap[parentIndex]; // 获取位移后的位置的任务
    if (parent !== undefined && compare(parent, node) > 0) { // 根据任务的sortIndex比较任务的大小,如果sortIndex相同,就根据任务的id比较;sortIndex就是任务的开始时间(currentTime + delay)。
      // The parent is larger. Swap positions.
      // 如果parent任务的开始时间比较晚,则和当前任务进行交互
      // 其实就是个排序算法,按任务开始时间or任务过期时间进行排序
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      // The parent is smaller. Exit.
      return;
    }
  }
}

// siftDown 方法,pop 后调用
// 这个方法一开始很不理解,为什么要把最后一个任务放到第一个任务位置上,再给它排序
// 后来想了想,个人感觉是为了避免优先级较低的任务长时间不执行
function siftDown(heap, node, i) {
  let index = i;
  const length = heap.length;
  while (index < length) {
    const leftIndex = (index + 1) * 2 - 1;
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;
    const right = heap[rightIndex];

    // If the left or right node is smaller, swap with the smaller of those.
    if (left !== undefined && compare(left, node) < 0) {
      if (right !== undefined && compare(right, left) < 0) {
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (right !== undefined && compare(right, node) < 0) {
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      // Neither child is smaller. Exit.
      return;
    }
  }
}

function compare(a, b) {
  // Compare sort index first, then task id.
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}