Длинная статья, которая поможет вам полностью понять принцип механизма планирования React.

React.js

нажмитеВойдите в репозиторий отладки исходного кода React.

Как независимый пакет, Scheduler может взять на себя ответственность за планирование задач самостоятельно, вам нужно только дать ему задачу и приоритет задачи, и он может помочь вам управлять задачами и организовывать выполнение задач. Вот как React и Scheduler работают вместе.

Для нескольких задач сначала будет выполняться задача с более высоким приоритетом. Для одной задачи он будет выполняться экономно. Другими словами, есть только один поток, и он не всегда будет занимать поток для выполнения задач. Вместо этого он выполняется некоторое время, прерывается и так далее. Этот режим используется, чтобы не использовать ограниченные ресурсы для выполнения трудоемких задач, решить проблему застревания страниц во время операций пользователя и добиться более быстрого отклика.

Мы можем выделить два важных поведения в планировщике:Управление несколькими задачами,Для выполнения однозадачного контроля.

основная концепция

Для достижения двух описанных выше вариантов поведения вводятся две концепции:приоритет задачи,квант времени.

Приоритет задач позволяет сортировать задачи в соответствии с их срочностью, чтобы задачи с наивысшим приоритетом выполнялись в первую очередь.

Временной квант определяет максимальное время выполнения одной задачи в этом кадре.Как только время выполнения задачи превысит временной квант, оно будет прервано, и задача будет выполняться на модерации. Это может гарантировать, что страница не будет зависать из-за непрерывного выполнения задачи слишком долго.

Обзор принципа

Основываясь на концепции приоритета задачи и временного интервала, Scheduler получил две основные функции вокруг своей основной цели - планирования задач: управление очередью задач, а также прерывание и восстановление задач в рамках временного интервала.

управление очередью задач

Управление очередью задач соответствует поведению управления многозадачностью Планировщика. Внутри планировщика задачи делятся на два типа: неистекшие и просроченные, которые хранятся в двух очередях, первые хранятся в timerQueue, а вторые — в taskQueue.

Как определить, просрочена ли задача?

Сравните время начала задачи (startTime) с текущим временем (currentTime). Если время начала больше текущего времени, это означает, что оно не истекло и помещается в timerQueue; если время начала меньше или равно текущему времени, это означает, что оно истекло и помещается в очередь timerQueue; очередь задач.

Как упорядочены задачи в разных очередях?

Когда задачи ставятся в очередь одна за другой, они естественным образом сортируются, чтобы гарантировать, что срочные задачи ранжируются первыми, поэтому сортировка основана на срочности задач. Критерии определения срочности задач в taskQueue и timerQueue разные.

  • В taskQueue задачи сортируются по времени истечения: чем раньше срок истечения, тем оно более срочное. Срок действия рассчитывается на основе приоритета задачи: чем выше приоритет, тем раньше срок действия.
  • В timerQueue задачи сортируются по времени старта (startTime) задач, чем раньше время старта, тем раньше он стартует, и тем меньше время старта стоит впереди. Когда задача поступает, время начала по умолчанию равно текущему времени.Если время задержки пропущено при входе в расписание, время начала представляет собой сумму текущего времени и времени задержки.

Задача ставится в две очереди, и что дальше?

Если он помещается в taskQueue, то немедленно запланируй функцию, которая будет зацикливать taskQueue и выполнять задачи в ней одну за другой.

Если она помещена в timerQueue, значит, задачи в ней не будут выполняться сразу, тогда дождитесь времени начала первой задачи в timerQueue, чтобы посмотреть, не истек ли срок действия задачи, и если да, то вынесите задачу из timerQueue.Поместите его в taskQueue, запланируйте функцию, чтобы зациклить его и выполнить задачи в нем, иначе он продолжит проверять, истекает ли срок действия первой задачи через некоторое время.

По сравнению с выполнением одной задачи управление очередью задач является концепцией макроуровня, которая использует приоритет задач для управления порядком задач в очереди задач и всегда позволяет обрабатывать самые срочные задачи в первую очередь.

Прерывание и восстановление отдельных задач

Прерывание и возобновление отдельной задачи соответствует поведению планировщика по управлению выполнением одной задачи. При зацикливании очереди задач для выполнения каждой задачи, если задача выполняется слишком долго и достигает предела кванта времени, задача должна быть прервана, чтобы освободить место для более важных вещей (таких как рисование в браузере) и т. д. Завершить и затем возобновите выполнение задачи.

Например этопример, нажмите кнопку, чтобы отобразить 140000 узлов DOM, чтобы позволить React запланировать длительную задачу обновления через планировщик. Перетаскивайте блоки одновременно, чтобы имитировать взаимодействие с пользователем. Задача обновления займет поток для выполнения задачи, и взаимодействие с пользователем также займет поток для ответа на страницу, которая определяет, что они являются взаимоисключающими. В параллельном режиме React после того, как задача обновления, запланированная планировщиком, сталкивается с взаимодействием с пользователем, это будет эффект в следующей анимации.

Выполнение задачи React и взаимодействие с откликом страницы являются взаимоисключающими, но поскольку планировщик может использовать квант времени, чтобы прервать задачу React, а затем передать поток браузеру для отрисовки, то есть в начале, на этапе построения волокна дерево, перетащите квадрат, чтобы получить своевременную обратную связь. Но позже он застрял, потому что построение дерева волокон было завершено, и оно перешло в стадию синхронной фиксации, из-за чего взаимодействие застряло. Анализ процесса рендеринга страницы может быть очень интуитивным, чтобы увидеть управление через временной интервал. Основная нить выделена для рисования страницы (Painting and Rendering, зеленый и фиолетовый).

Для достижения такого эффекта планирования планировщику нужны две роли:планировщик задач, исполнитель задач. Планировщик назначает исполнителя, а исполнитель зацикливает очередь задач и выполняет задачи одну за другой. Когда время выполнения задачи относительно велико, исполнитель прервет выполнение задачи в соответствии с интервалом времени, а затем сообщит планировщику: задача, которую я сейчас выполняю, была прервана, и часть ее не была завершена. , но теперь она должна уступить место более важным задачам, вы можете назначить другого исполнителя, чтобы задачу можно было продолжить выполнять позже (восстановление задачи). Таким образом, планировщик знает, что задача не была завершена и ее нужно продолжать выполнять, и он назначит другого исполнителя для продолжения выполнения задачи.

Благодаря сотрудничеству исполнителя и планировщика может быть реализовано прерывание и восстановление задачи.

Резюме принципа

Планировщик управляет двумя очередями, taskQueue и timerQueue.Он периодически помещает просроченные задачи из timerQueue в taskQueue, а затем позволяет планировщику уведомить исполнителя о циклическом выполнении TaskQueue для выполнения каждой задачи. Исполнитель контролирует выполнение каждой задачи, как только время выполнения задачи превышает лимит временного интервала. будет прервано, а затем текущий исполнитель завершит работу. Перед выходом планировщик будет уведомлен о назначении нового исполнителя для продолжения выполнения задачи. Новый исполнитель по-прежнему будет прерывать задачу в соответствии с интервалом времени при выполнении задачи, а затем выйдите.Повторяйте этот процесс, пока текущая задача не будет полностью завершена, удалите задачу из очереди задач. Каждая задача в taskQueue обрабатывается таким образом, и, наконец, все задачи завершаются, что является полным рабочим процессом Планировщика.

Здесь есть ключевой момент, то есть как исполнитель узнает, выполнена задача или нет? Это еще одна тема, по которой стоит судить о статусе завершения задачи. Акцент делается при объяснении деталей задач, выполняемых исполнителем.

Выше приведен обзор принципа планировщика, а далее подробно рассмотрен механизм совместной работы React и планировщика. Он включает в себя связь между React и Scheduler, планирование записи, приоритет задачи, время истечения срока действия задачи, прерывание задачи и восстановление, а также оценку статуса завершения задачи.

Подробный процесс

Прежде чем мы начнем, давайте взглянем на схему системы, сформированной React и Scheduler.

Вся система делится на три части:

  • Где порождается задача: React
  • Переводчик для обменов React и Scheduler: SchedulerWithReactIntegration
  • Планировщик задач: Планировщик

Следующий код используется в React, чтобы позволить задаче построения дерева волокон войти в процесс планирования:

scheduleCallback(
  schedulerPriorityLevel,
  performConcurrentWorkOnRoot.bind(null, root),
);

Задача передается планировщику через переводчика, а планировщик выполняет реальное планирование задачи, так зачем вам роль переводчика?

Связь между React и Scheduler

Scheduler помогает React планировать различные задачи, но по сути это две совершенно несвязанные вещи, каждая из которых имеет свой механизм приоритета, поэтому для их соединения нужна промежуточная роль.

На самом деле такой файл предусмотрен в react-reconciler специально для такой работы.SchedulerWithReactIntegration.old(new).js. Он переводит их приоритеты, чтобы React и Scheduler могли понимать друг друга. Кроме того, некоторые функции в Scheduler инкапсулированы для использования React.

Важные файлы для выполнения задач ReactReactFiberWorkLoop.js, содержание Планировщика взято изSchedulerWithReactIntegration.old(new).jsимпортный. Его можно понимать как мост между React и Scheduler.

// ReactFiberWorkLoop.js
import {
  scheduleCallback,
  cancelCallback,
  getCurrentPriorityLevel,
  runWithPriority,
  shouldYield,
  requestPaint,
  now,
  NoPriority as NoSchedulerPriority,
  ImmediatePriority as ImmediateSchedulerPriority,
  UserBlockingPriority as UserBlockingSchedulerPriority,
  NormalPriority as NormalSchedulerPriority,
  flushSyncCallbackQueue,
  scheduleSyncCallback,
} from './SchedulerWithReactIntegration.old';

SchedulerWithReactIntegration.old(new).jsИнкапсулируя содержимое планировщика, React предоставляет две функции ввода расписания:scheduleCallbackа такжеscheduleSyncCallback. Задача входит в процесс планирования через функцию ввода планирования.

Например, задача построения дерева волокон передается в параллельном режиме.scheduleCallbackЗаполните расписание, в режиме синхронного рендерингаscheduleSyncCallbackЗаканчивать.

// concurrentMode
// 将本次更新任务的优先级转化为调度优先级
// schedulerPriorityLevel为调度优先级
const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
  newCallbackPriority,
);
// concurrent模式
scheduleCallback(
  schedulerPriorityLevel,
  performConcurrentWorkOnRoot.bind(null, root),
);

// 同步渲染模式
scheduleSyncCallback(
  performSyncWorkOnRoot.bind(null, root),
)

Оба они на самом деле являются инкапсуляциями scheduleCallback в Scheduler, но входящий приоритет отличается: первый передает приоритет расписания, рассчитанный обновленной на этот раз полосой, а второй — высший уровень приоритета. Еще одно отличие состоит в том, что первый напрямую передает задачу планировщику, а второй сначала помещает задачу в очередь синхронизации SchedulerWithReactIntegration.old(new).js, а затем передает планировщику функцию, выполняющую очередь синхронизации, с наивысший приоритет Планирование, поскольку передается наивысший приоритет, означает, что это будет задача, срок действия которой истекает немедленно, и она будет выполнена немедленно, что гарантирует выполнение задачи в следующем цикле событий.

function scheduleCallback(
  reactPriorityLevel: ReactPriorityLevel,
  callback: SchedulerCallback,
  options: SchedulerCallbackOptions | void | null,
) {
  // 将react的优先级翻译成Scheduler的优先级
  const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
  // 调用Scheduler的scheduleCallback,传入优先级进行调度
  return Scheduler_scheduleCallback(priorityLevel, callback, options);
}

function scheduleSyncCallback(callback: SchedulerCallback) {
  if (syncQueue === null) {
    syncQueue = [callback];
    // 以最高优先级去调度刷新syncQueue的函数
    immediateQueueCallbackNode = Scheduler_scheduleCallback(
      Scheduler_ImmediatePriority,
      flushSyncCallbackQueueImpl,
    );
  } else {
    syncQueue.push(callback);
  }
  return fakeCallbackNode;
}

Приоритет в планировщике

Говоря о приоритетах, давайте взглянем на собственные уровни приоритета планировщика, которые определяют следующие уровни приоритета для задач:

export const NoPriority = 0; // 没有任何优先级
export const ImmediatePriority = 1; // 立即执行的优先级,级别最高
export const UserBlockingPriority = 2; // 用户阻塞级别的优先级
export const NormalPriority = 3; // 正常的优先级
export const LowPriority = 4; // 较低的优先级
export const IdlePriority = 5; // 优先级最低,表示任务可以闲置

Была упомянута роль приоритета задачи, которая является важной основой для расчета времени истечения срока действия задачи и связана с сортировкой просроченных задач в taskQueue.

// 不同优先级对应的不同的任务过期时间间隔
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;

...

// 计算过期时间(scheduleCallback函数中的内容)
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;
}

// startTime可暂且认为是当前时间
var expirationTime = startTime + timeout;

Видно, что время истечения — это время начала задачи плюс тайм-аут, и этот тайм-аут рассчитывается по приоритету задачи.

Более подробное объяснение приоритетов в React содержится в этой статье, которую я написал:Приоритет в реакции

Запись расписания - scheduleCallback

Из вышеприведенного прочесывания мы знаем, что scheduleCallback в планировщике является ключевой точкой, с которой начинается процесс планирования. Прежде чем вводить эту запись планирования, давайте сначала разберемся, что такое задачи в Планировщике:

  var newTask = {
    id: taskIdCounter++,
    // 任务函数
    callback,
    // 任务优先级
    priorityLevel,
    // 任务开始的时间
    startTime,
    // 任务的过期时间
    expirationTime,
    // 在小顶堆队列中排序的依据
    sortIndex: -1,
  };
  • обратный вызов: реальная функция задачи, ключевым моментом является функция задачи, переданная извне, например функция задачи для построения дерева волокон: PerformConcurrentWorkOnRoot
  • priorityLevel: приоритет задачи, участвующий в расчете времени истечения задачи.
  • startTime: указывает время запуска задачи, влияющее на ее сортировку в timerQueue.
  • expireTime: указывает, когда срок действия задачи истекает, влияя на ее порядок в taskQueue.
  • sortIndex: основа для сортировки в малой верхней очереди кучи.После определения того, просрочена задача или нет, sortIndex будет присвоено значение expireTime или startTime, что обеспечивает основу сортировки для двух малых верхних очередей кучи (taskQueue, timerQueue).

Настоящая сутьcallback, как функция задачи, результат ее выполнения повлияет на оценку состояния завершения задачи. Теперь давайте посмотримscheduleCallbackСписок задач:Он отвечает за создание задач планирования, размещение задач в timerQueue или taskQueue в зависимости от того, истек ли срок действия задач, а затем инициирование поведения планирования, чтобы позволить задачам войти в планирование.. Полный код выглядит следующим образом:

function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 获取当前时间,它是计算任务开始时间、过期时间和判断任务是否过期的依据
  var currentTime = getCurrentTime();
  // 确定任务开始时间
  var startTime;
  // 从options中尝试获取delay,也就是推迟时间
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      // 如果有delay,那么任务开始时间就是当前时间加上delay
      startTime = currentTime + delay;
    } else {
      // 没有delay,任务开始时间就是当前时间,也就是任务需要立刻开始
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }

  // 计算timeout
  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 ms
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT; // 10000
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT; // 5000
      break;
  }
  // 计算任务的过期时间,任务开始时间 + timeout
  // 若是立即执行的优先级(ImmediatePriority),
  // 它的过期时间是startTime - 1,意味着立刻就过期
  var expirationTime = startTime + timeout;

  // 创建调度任务
  var newTask = {
    id: taskIdCounter++,
    // 真正的任务函数,重点
    callback,
    // 任务优先级
    priorityLevel,
    // 任务开始的时间,表示任务何时才能执行
    startTime,
    // 任务的过期时间
    expirationTime,
    // 在小顶堆队列中排序的依据
    sortIndex: -1,
  };

  // 下面的if...else判断各自分支的含义是:

  // 如果任务未过期,则将 newTask 放入timerQueue, 调用requestHostTimeout,
  // 目的是在timerQueue中排在最前面的任务的开始时间的时间点检查任务是否过期,
  // 过期则立刻将任务加入taskQueue,开始调度

  // 如果任务已过期,则将 newTask 放入taskQueue,调用requestHostCallback,
  // 开始调度执行taskQueue中的任务
  if (startTime > currentTime) {
    // 任务未过期,以开始时间作为timerQueue排序的依据
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // 如果现在taskQueue中没有任务,并且当前的任务是timerQueue中排名最靠前的那一个
      // 那么需要检查timerQueue中有没有需要放到taskQueue中的任务,这一步通过调用
      // requestHostTimeout实现
      if (isHostTimeoutScheduled) {
        // 因为即将调度一个requestHostTimeout,所以如果之前已经调度了,那么取消掉
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // 调用requestHostTimeout实现任务的转移,开启调度
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 任务已经过期,以过期时间作为taskQueue排序的依据
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);

    // 开始执行任务,使用flushWork去执行taskQueue
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

Основное внимание в этом процессе уделяется обработке того, истекает срок действия задачи или нет.

Для неистекших задач они будут помещены в timerQueue, расставлены по времени начала, а затем вызваныrequestHostTimeout, чтобы подождать какое-то время, дождитесь времени старта задачи, которая должна стартовать самой ранней в timerQueue (первой задаче в очереди), а затем проверьте, истекает ли срок ее действия, и если истекает, поместите ее в taskQueue, чтобы задача могла быть выполнена, в противном случае продолжайте ждать. Этот процесс проходитhandleTimeoutЗаканчивать.

handleTimeoutОбязанности:

  • передачаadvanceTimers, Проверьте просроченные задачи в очереди timerQueue и поместите их в taskQueue.
  • Проверьте, началось ли планирование.Если оно не было запланировано, проверьте, есть ли уже задача в taskQueue:
    • Если есть, и он простаивает сейчас, это означает, что предыдущие AdvanceTimers поставили срок действия съемной задачи на CassQueue, затем начните планирование немедленно и выполнить задачу
    • Если нет, и он сейчас простаивает, что указывает на то, что предыдущие advanceTimers не проверяли наличие задач с истекшим сроком действия в timerQueue, то вызовите сноваrequestHostTimeoutПовторите этот процесс.

Короче говоря, все задачи в timerQueue должны быть переданы в taskQueue для выполнения.

Для задачи с истекшим сроком действия после помещения ее в очередь задач вызовитеrequestHostCallback, позволить планировщику запланировать выполнение задачи исполнителем, что означает начало процесса планирования.

Начать планирование - узнать планировщика и исполнителя

Планировщик по телефонуrequestHostCallbackПозвольте задаче войти в процесс планирования и просмотрите место, где scheduleCallback, наконец, вызывает requestHostCallback для выполнения указанной выше задачи:

if (!isHostCallbackScheduled && !isPerformingWork) {
  isHostCallbackScheduled = true;
  // 开始进行调度
  requestHostCallback(flushWork);
}

Поскольку это ставитflushWorkВ качестве входных данныхИсполнительПо сути, это вызовflushWork, нас не волнует как исполнитель выполняет задачу, мы сначала обращаем внимание на то, как она запланирована, нам нужно сначала узнатьпланировщик, на что нужно смотретьrequestHostCallbackРеализация:

Планировщик различает среду браузера и среду без браузера, дляrequestHostCallbackДелаются две разные реализации. В среде без браузера используйте setTimeout.

  requestHostCallback = function(cb) {
    if (_callback !== null) {
      setTimeout(requestHostCallback, 0, cb);
    } else {
      _callback = cb;
      setTimeout(_flushCallback, 0);
    }
  };

В среде браузера, реализованной с помощью MessageChannel, о MessageChannelпредставлятьЯ не буду вдаваться в подробности.


  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;


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

Причина, по которой существует две реализации, заключается в том, что в небраузерной среде нет частоты обновления экрана, и нет понятия кадров, поэтому нет кванта времени, что принципиально отличается от выполнения задач в браузерной среде, т.к. небраузерная среда В основном отсутствует взаимодействие с пользователем, поэтому в этом сценарии не определяется, превышает ли время выполнения задачи ограничение временного интервала, и выполнение задач в браузерной среде будет ограничено временным интервалом. В остальном, несмотря на то, что реализация в двух средах отличается, она делает более или менее одно и то же.

Сначала посмотрите на небраузерную среду, которая хранит входные параметры (функции, выполняющие задачи) во внутренние переменные._callbackвключи, затем запланируй_flushCallbackДля выполнения этой переменной _callback очередь задач очищается.

Снова взглянув на среду браузера, она сохраняет входные параметры (функции, выполняющие задачи) во внутренние переменные.scheduledHostCallback, а затем отправить сообщение через порт MessageChannel, пустьchannel.port1Функция прослушиванияperformWorkUntilDeadlineбыть казненным.performWorkUntilDeadlineбудет выполняться внутриscheduledHostCallback, и, наконец, taskQueue очищается.

Из вышеприведенного описания можно четко определить планировщик: небраузерная средаsetTimeout, среда браузераport.postMessage. Исполнители двух сред также очевидны, первая_flushCallback, последнийperformWorkUntilDeadline, все, что делает исполнитель, — это вызывает фактическую функцию выполнения задачи.

Поскольку в этой статье основное внимание уделяется поведению планировщика при планировании временных интервалов, в ней в основном обсуждается поведение планирования в среде браузера.executeWorkUntilDeadline включает вызов функции выполнения задачи для выполнения задачи.Прерывание и возобновление задач,Оценка статуса завершения задачи, следующее содержание будет сосредоточено на этих двух моментах.

Выполнение задачи - начиная с PerformWorkUntilDeadline

Упоминается в обзоре принципов в начале статьиperformWorkUntilDeadlineКак исполнитель, его роль состоит в том, чтобы прервать задачу в соответствии с ограничением временного интервала и уведомить планировщик о назначении нового исполнителя для продолжения задачи. Глядя на его реализацию в соответствии с этим восприятием, это будет очень ясно.

  const performWorkUntilDeadline = () => {

    if (scheduledHostCallback !== null) {
      // 获取当前时间
      const currentTime = getCurrentTime();

      // 计算deadline,deadline会参与到
      // shouldYieldToHost(根据时间片去限制任务执行)的计算中
      deadline = currentTime + yieldInterval;
      // hasTimeRemaining表示任务是否还有剩余时间,
      // 它和时间片一起限制任务的执行。如果没有时间,
      // 或者任务的执行时间超出时间片限制了,那么中断任务。

      // 它的默认为true,表示一直有剩余时间
      // 因为MessageChannel的port在postMessage,
      // 是比setTimeout还靠前执行的宏任务,这意味着
      // 在这一帧开始时,总是会有剩余时间
      // 所以现在中断任务只看时间片的了
      const hasTimeRemaining = true;
      try {
        // scheduledHostCallback去执行任务的函数,
        // 当任务因为时间片被打断时,它会返回true,表示
        // 还有任务,所以会再让调度者调度一个执行者
        // 继续执行任务
        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;
  };

performWorkUntilDeadlineВнутренний вызовscheduledHostCallback, он устанавливается уже в начале планированияrequestHostCallbackназначить наflushWork, вы можете обратиться к выше, чтобы просмотретьrequestHostCallbackреализация.

flushWorkКак функция, которая фактически выполняет задачу, она будет зацикливать очередь задач и вызывать функции задачи в ней одну за другой. Давайте взглянемflushWorkЧто именно сделал.

function flushWork(hasTimeRemaining, initialTime) {

  ...

  return workLoop(hasTimeRemaining, initialTime);

  ...

}

это звонитworkLoopи вернуть результат своего вызова. Итак, теперь основное содержание выполнения задачи, по-видимому,workLoopбинго.workLoopВызов приводит к окончательному выполнению задачи.

Прерывание задачи и восстановление

чтобы понятьworkLoop, необходимо пересмотреть одну из функций планировщика: ограничение времени выполнения задач через временные срезы. Затем, поскольку выполнение задачи ограничено, она должна быть незавершенной, если незавершенная задача прервана, ее необходимо возобновить.

Таким образом, выполнение задачи в рамках кванта времени имеет следующие важные характеристики:будет прервано и будет возобновлено.

Нетрудно догадаться,workLoopКак функция, которая фактически выполняет задачу, то, что она делает, определенно связано с восстановлением прерывания задачи. Давайте сначала посмотрим на его структуру:

function workLoop(hasTimeRemaining, initialTime) {

  // 获取taskQueue中排在最前面的任务
  currentTask = peek(taskQueue);
  while (currentTask !== null) {

    if (currentTask.expirationTime > currentTime &&
     (!hasTimeRemaining || shouldYieldToHost())) {
       // break掉while循环
       break
    }

    ...
    // 执行任务
    ...

    // 任务执行完毕,从队列中删除
    pop(taskQueue);

    // 获取下一个任务,继续循环
    currentTask = peek(taskQueue);
  }


  if (currentTask !== null) {
    // 如果currentTask不为空,说明是时间片的限制导致了任务中断
    // return 一个 true告诉外部,此时任务还未执行完,还有任务,
    // 翻译成英文就是hasMoreWork
    return true;
  } else {
    // 如果currentTask为空,说明taskQueue队列中的任务已经都
    // 执行完了,然后从timerQueue中找任务,调用requestHostTimeout
    // 去把task放到taskQueue中,到时会再次发起调度,但是这次,
    // 会先return false,告诉外部当前的taskQueue已经清空,
    // 先停止执行任务,也就是终止任务调度

    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }

    return false;
  }
}

WorkLoop можно разделить на две части: циклические задачи выполнения TaskQueue и оценка статуса задачи.

Тиражная задачаОчередь миссии

На данный момент, независимо от того, как выполняется задача, просто сосредоточьтесь на том, как задача ограничена временным интервалом.

if (currentTask.expirationTime > currentTime &&
     (!hasTimeRemaining || shouldYieldToHost())) {
   // break掉while循环
   break
}

currentTask — текущая выполняющаяся задача.Условие ее завершения: срок действия задачи не истек, но нет оставшегося времени (поскольку hasTimeRemaining всегда истинно, что связано со временем выполнения MessageChannel как макрозадачи, мы игнорируем это условие суждения, Только следить за временным интервалом), либо следует отдать право на выполнение основному потоку (временной интервал ограничен), то есть currentTask выполняется хорошо, но время не позволяет, тогда вы можете только сначала разорвите цикл while, так что на этот раз логика, выполняемая currentTask в цикле, не может быть выполнена (Вот ключ к прерыванию задачи). Но нарушается только цикл while, а нижняя часть цикла по-прежнему будет судить о статусе currentTask.

Поскольку это только прервано, currentTask не может быть нулевым, поэтому он вернет true, чтобы сообщить снаружи, что он не завершен (Вот ключ к задаче восстановления), в противном случае это означает, что все задачи были выполнены, очередь задач была очищена, верните false, чтобы внешнийПрекратить это расписание. Результат выполнения workLoop будет возвращен flushWork, на самом деле flushWorkscheduledHostCallback,когдаperformWorkUntilDeadlineобнаруженscheduledHostCallbackКогда возвращаемое значение (hasMoreWork) равно false, планирование останавливается.

рассмотрениеperformWorkUntilDeadlineПоведение в , механизм восстановления прерывания задачи можно четко связать последовательно:

  const performWorkUntilDeadline = () => {

    ...

    const hasTimeRemaining = true;
    // scheduledHostCallback去执行任务的函数,
    // 当任务因为时间片被打断时,它会返回true,表示
    // 还有任务,所以会再让调度者调度一个执行者
    // 继续执行任务
    const hasMoreWork = scheduledHostCallback(
      hasTimeRemaining,
      currentTime,
    );

    if (!hasMoreWork) {
      // 如果没有任务了,停止调度
      isMessageLoopRunning = false;
      scheduledHostCallback = null;
    } else {
      // 如果还有任务,继续让调度者调度执行者,便于继续
      // 完成任务
      port.postMessage(null);
    }
  };

Когда задача прерывается,performWorkUntilDeadlineЗатем планировщик вызовет исполнителя, чтобы продолжить выполнение задачи, пока задача не будет завершена. Но здесь есть важный момент: как судить о том, выполнена ли задача? Это требует исследованияworkLoopЧасть логики, выполняющая задачу.

Определение статуса завершения отдельной задачи

Прерывание восстановления задачи — это итеративный процесс, который повторяется до тех пор, пока задача не завершится. Поэтому очень важно судить о том, выполнена ли задача, а не выполненной ли будет задача.Функция повтора задачи.

В качестве аналогии можно использовать рекурсивную функцию: если граница рекурсии не достигнута, она повторно вызывает сама себя. Эта граница рекурсии является признаком завершения задачи. Поскольку задача, обрабатываемая рекурсивной функцией, является самой собой, удобно использовать завершение задачи в качестве рекурсивной границы для завершения задачи, но планировщик в планировщикеworkLoopВ отличие от рекурсии, это только задача, которая выполняет задачи. Эта задача не генерируется сама по себе, а является внешней (например, она выполняет рабочий цикл React для рендеринга дерева волокон). Она может многократно выполнять функции задачи, но граница ( то есть завершена ли задача) нельзя получить напрямую, как рекурсию, и можно судить только по возвращаемому значению функции задачи. который:Если возвращаемое значение функции задачи является функцией, это означает, что текущая задача не выполнена, и нужно продолжить вызов функции задачи, иначе задача завершена.workLoopв этом случаеОпределение статуса завершения отдельной задачи.

действительно объясняяworkLoopПрежде чем приступить к логике выполнения задачи в , давайте воспользуемся примером, чтобы понять суть оценки состояния завершения задачи.

Существует задача calculate, которая отвечает за увеличение currentResult на 1 каждый раз, пока не достигнет 3. Когда значение 3 не достигнуто, calculate не вызывает себя, а возвращает себя. Таким образом, снаружи можно узнать, завершил ли расчёт задачу.

const result = 3
let currentResult = 0
function calculate() {
    currentResult++
    if (currentResult < result) {
        return calculate
    }
    return null
}

Вышеупомянутая задача Далее мы моделируем планирование и выполняем расчет. Однако выполнение должно быть основано на временных срезах, чтобы наблюдать эффект, используйте setInterval только для имитации механизма возобновления задачи из-за прерывания временного среза (довольно грубое моделирование, просто поймите, что это моделирование времени). срезы, и ориентироваться на статус завершения задачи. Суждение), выполнять ее раз в 1 секунду, то есть за один раз выполняется только треть всех задач.

Кроме того, в планировщике есть две очереди для управления задачами, пока мы используем только одну очередь (taskQueue) для хранения задач. Кроме того, требуются три роли: функция, добавляющая задачу в расписание (запись расписания scheduleCallback), функция, запускающая планирование (requestHostCallback), и функция, выполняющая задачу (workLoop, где лежит ключевая логика). .

const result = 3
let currentResult = 0

function calculate() {
    currentResult++
    if (currentResult < result) {
        return calculate
    }
    return null
}

// 存放任务的队列
const taskQueue = []
// 存放模拟时间片的定时器
let interval

// 调度入口----------------------------------------
const scheduleCallback = (task, priority) => {
    // 创建一个专属于调度器的任务
    const taskItem = {
        callback: task,
        priority
    }

    // 向队列中添加任务
    taskQueue.push(taskItem)
    // 优先级影响到任务在队列中的排序,将优先级最高的任务排在最前面
    taskQueue.sort((a, b) => (a.priority - b.priority))
    // 开始执行任务,调度开始
    requestHostCallback(workLoop)
}
// 开始调度-----------------------------------------
const requestHostCallback = cb => {
    interval = setInterval(cb, 1000)
}
// 执行任务-----------------------------------------
const workLoop = () => {
    // 从队列中取出任务
    const currentTask = taskQueue[0]
    // 获取真正的任务函数,即calculate
    const taskCallback = currentTask.callback
    // 判断任务函数否是函数,若是,执行它,将返回值更新到currentTask的callback中
    // 所以,taskCallback是上一阶段执行的返回值,若它是函数类型,则说明上一次执行返回了函数
    // 类型,说明任务尚未完成,本次继续执行这个函数,否则说明任务完成。
    if (typeof taskCallback === 'function') {
        currentTask.callback = taskCallback()
        console.log('正在执行任务,当前的currentResult 是', currentResult);
    } else {
        // 任务完成。将当前的这个任务从taskQueue中移除,并清除定时器
        console.log('任务完成,最终的 currentResult 是', currentResult);
        taskQueue.shift()
        clearInterval(interval)
    }
}

// 把calculate加入调度,也就意味着调度开始
scheduleCallback(calculate, 1)

Окончательный результат выполнения следующий:

正在执行任务,当前的currentResult 是 1
正在执行任务,当前的currentResult 是 2
正在执行任务,当前的currentResult 是 3
任务完成,最终的 currentResult 是 3

Видно, что если его не прибавить к 3, то calculate вернется сам,Если workLoop считает, что возвращаемое значение является функцией, это означает, что задача не была завершена, и он будет продолжать вызывать функцию задачи для завершения задачи..

В этом примере сохранена только логика оценки статуса выполнения задачи в workLoop, а все остальное не идеально. Будет превалировать настоящий workLoop. Теперь давайте выложим весь его код и полностью посмотрим на реальную реализацию:

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  // 开始执行前检查一下timerQueue中的过期任务,
  // 放到taskQueue中
  advanceTimers(currentTime);
  // 获取taskQueue中最紧急的任务
  currentTask = peek(taskQueue);

  // 循环taskQueue,执行任务
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 时间片的限制,中断任务
      break;
    }
    // 执行任务 ---------------------------------------------------
    // 获取任务的执行函数,这个callback就是React传给Scheduler
    // 的任务。例如:performConcurrentWorkOnRoot
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      // 如果执行函数为function,说明还有任务可做,调用它
      currentTask.callback = null;
      // 获取任务的优先级
      currentPriorityLevel = currentTask.priorityLevel;
      // 任务是否过期
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      // 获取任务函数的执行结果
      const continuationCallback = callback(didUserCallbackTimeout);
      if (typeof continuationCallback === 'function') {
        // 检查callback的执行结果返回的是不是函数,如果返回的是函数,则将这个函数作为当前任务新的回调。
        // concurrent模式下,callback是performConcurrentWorkOnRoot,其内部根据当前调度的任务
        // 是否相同,来决定是否返回自身,如果相同,则说明还有任务没做完,返回自身,其作为新的callback
        // 被放到当前的task上。while循环完成一次之后,检查shouldYieldToHost,如果需要让出执行权,
        // 则中断循环,走到下方,判断currentTask不为null,返回true,说明还有任务,回到performWorkUntilDeadline
        // 中,判断还有任务,继续port.postMessage(null),调用监听函数performWorkUntilDeadline(执行者),
        // 继续调用workLoop行任务

        // 将返回值继续赋值给currentTask.callback,为得是下一次能够继续执行callback,
        // 获取它的返回值,继续判断任务是否完成。
        currentTask.callback = continuationCallback;
      } else {
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }
    // 从taskQueue中继续获取任务,如果上一个任务未完成,那么它将不会
    // 被从队列剔除,所以获取到的currentTask还是上一个任务,会继续
    // 去执行它
    currentTask = peek(taskQueue);
  }
  // return 的结果会作为 performWorkUntilDeadline
  // 中判断是否还需要再次发起调度的依据
  if (currentTask !== null) {
    return true;
  } else {
    // 若任务完成,去timerQueue中找需要最早开始执行的那个任务
    // 调度requestHostTimeout,目的是等到了它的开始事件时把它
    // 放到taskQueue中,再次调度
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

так,workLoop определяет состояние завершения задачи, оценивая возвращаемое значение функции задачи..

Суммируйте общую взаимосвязь между оценкой состояния завершения задачи и выполнением задачи: когда начинается планирование, планировщик назначает исполнителю выполнение задачи, которая на самом деле является обратным вызовом (то есть функцией задачи) в задаче выполнения. Если исполнитель решит, что возвращаемое значение обратного вызова является функцией, указывая на то, что оно не завершено, он снова назначит возвращенную функцию обратному вызову задачи.Поскольку задача не была завершена, она не будет удалена из очереди задач, и currentTask все равно ее получит. , цикл while будет продолжать выполнять эту задачу до следующего раза, пока задача не будет завершена и не исключена из очереди, он перейдет к следующей.

Еще один момент, который следует упомянуть, — это функция задачи для построения дерева волокон:performConcurrentWorkOnRoot, параметр, который он принимает, — fiberRoot.

function performConcurrentWorkOnRoot(root) {
  ...
}

В workLoop это будет вызываться так (обратный вызовperformConcurrentWorkOnRoot):

const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
const continuationCallback = callback(didUserCallbackTimeout);

didUserCallbackTimeoutОчевидно, что это значение логического типа, а не fiberRoot, но выполним ConcurrentWorkOnRoot можно нормально. Это связано с тем, что корень передается во время привязки в начале планирования и самого последующего возврата.

// 调度的时候
scheduleCallback(
  schedulerPriorityLevel,
  performConcurrentWorkOnRoot.bind(null, root),
);

// 其内部return自身的时候
function performConcurrentWorkOnRoot(root) {
  ...

  if (root.callbackNode === originalCallbackNode) {
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}

В этом случае передайте ему параметр для вызова, тогда этот параметр можно будет получить только как последующий параметр,performConcurrentWorkOnRootПервый полученный параметр — это корень, передаваемый при связывании, который связан с реализацией связывания. Вы можете запустить следующий простой пример:

function test(root, b) {
    console.log(root, b)
}
function runTest() {
    return test.bind(null, 'root')
}

runTest()(false)

// 结果:root false

Выше приведены две основные логики планировщика при выполнении задач: прерывание задачи и оценка состояния восстановления и завершения задачи. Они работают вместе, поэтому, если задача прерывается до ее завершения, новый исполнитель расписания возобновляет выполнение задачи до ее завершения. К этому моменту основная часть Планировщика написана, и далее следует логика отмены планирования.

Отменить расписание

Из приведенного выше содержания мы знаем, что выполнение задачи на самом деле является обратным вызовом выполняемой задачи. Когда обратный вызов является функцией, он выполняется. Что происходит, когда он равен нулю? Текущая задача будет удалена из taskQueue, давайте еще раз посмотрим на функцию workLoop:

function workLoop(hasTimeRemaining, initialTime) {
  ...

  // 获取taskQueue中最紧急的任务
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    ...
    const callback = currentTask.callback;

    if (typeof callback === 'function') {
      // 执行任务
    } else {
      // 如果callback为null,将任务出队
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  ...
}

Таким образом, ключом к отмене планирования является установка обратного вызова текущей задачи на ноль.

function unstable_cancelCallback(task) {

  ...

  task.callback = null;
}

Почему установка обратного вызова на null может отменить планирование задачи? Потому что в рабочем цикле, если обратный вызов имеет значение null, он будет удален из очереди задач, поэтому текущая задача больше не будет выполняться. Он отменяет выполнение текущей задачи, и цикл while продолжит выполнение следующей задачи.

Каков сценарий отмены задачи в React? Когда выполняется задача обновления, внезапно появляется задача с высоким приоритетом, и текущая задача должна быть отменена, что является лишь одним из многих сценариев.

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {

  ...

  if (existingCallbackNode !== null) {
    const existingCallbackPriority = root.callbackPriority;
    if (existingCallbackPriority === newCallbackPriority) {
      return;
    }
    // 取消掉原有的任务
    cancelCallback(existingCallbackNode);
  }

  ...
}

Суммировать

Планировщик использует приоритет задач для обеспечения многозадачного управления, приоритизации высококачественных задач и использования непрерывного планирования задач для решения проблемы прерывания и восстановления одной задачи, вызванной временными интервалами. Результат выполнения функции задачи указывает, следует ли прекратить планирование текущей задачи. Кроме того, выполнение части задачи в течение ограниченного промежутка времени также гарантирует, что браузер отреагирует на взаимодействие и завершит его. задание.