Почему React Scheduler использует реализацию MessageChannel

React.js
Почему React Scheduler использует реализацию MessageChannel

TL;DR

Эта статья включает в себя:

  1. Планировщик профилей - разделение времени
  2. Должен ли сегмент времени выбирать микрозадачи или макрозадачи
  3. Почему бы не выбрать setTimeout(fn, 0)
  4. Почему бы не выбрать requestAnimationFrame(fn)

Введение в планировщик — разделение времени

Если «процесс рендеринга компонента занимает много времени» или «в фазе согласования участвует много виртуальных DOM-узлов», то выполнение этапа согласования всех компонентов одновременно займет много времени.

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

Цель архитектуры Fiber состоит в том, чтобы «иметь возможность выполнять фазу согласования каждой виртуальной DOM независимо», а не каждый раз выполнять фазу согласования всего дерева виртуального DOM.

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

Взаимодействие React и Scheduler

Если рассматривать только взаимодействие между React и Scheduler, то процесс обновления компонента выглядит следующим образом:

  1. Состояние компонента React обновляется, а задача сохраняется в планировщике, что является алгоритмом обновления React.
  2. Планировщик планирует задачу для выполнения алгоритма обновления React.
  3. После того, как React обновляет волокно на этапе согласования, он спрашивает планировщик, нужно ли ему приостановить работу. Если пауза не требуется, повторите шаг 3, чтобы продолжить обновление следующего волокна.
  4. Если планировщик говорит, что он должен сделать паузу, отреагируйте, вернут функцию, которая сообщает планировцу, что задача еще не закончена. Планировщик запланирует задачу в какой-то момент в будущем.

На первом этапе планировщику необходимо предоставитьpushTask()метод, с помощью которого React хранит задачи.

На втором этапе Планировщику необходимо открытьscheduleTask()способ планирования задач.

На третьем шаге Планировщику необходимо выставитьshouldYield()метод, с помощью которого React решает, следует ли приостановить выполнение задачи.

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

Этот процесс может быть выражен в следующем псевдокоде:

const scheduler = {
  pushTask() {
    // 1. 存入任务
  },

  scheduleTask() {
    // 2. 挑选一个任务并执行
    const task = pickTask()
    const hasMoreTask = task()

    if (hasMoreTask) {
      // 4. 未来继续调度
    }
  },

  shouldYield() {
    // 3. 由调用方调用,调用方判断是否需要暂停
  },
}

// 当用户点击时修改了组件状态,则伪代码如下
const handleClick = () => {
  // React 组件更新时,产生任务
  const task = () => {
    const fiber = root
    while (!scheduler.shouldYield() && fiber) {
      // reconciliation() 对当前的 fiber 执行调和阶段
      // 并返回下一个 fiber
      fiber = reconciliation(fiber)
    }
  }

  scheduler.pushTask(task)

  // React 会在将来某个时间执行 scheduler.scheduleTask()
  // 这里假设立即执行 scheduler.scheduleTask()
  scheduler.scheduleTask()
}

Планировщик — это общий дизайн, а не только для React.

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

В качестве примера (ради примера) предположим, что мы хотим вычислить1000Сумма целых чисел, код для одноразового обхода выглядит следующим образом:

let sum = 0
for (let i = 0; i < 1000; ++i) {
  sum += arr[i]
}

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

const task = () => {
  let pos = 0
  let sum = 0
  const continuousExec = () => {
    for (; !scheduler.shouldYield() && pos < 1000; ++pos) {
      sum += arr[i]
    }

    if (pos === 1000) {
      return
    }

    return continuousExec
  }

  return continuousExec()
}

когдаscheduler.shouldYield()вернутьtrueКогда задача выполняется, выполнение задачи приостанавливается, и браузер может обновить страницу в это время, чтобы избежать зависания страницы.

Метод планирования Scheduler можно понимать так: выполняемая в данный момент функция возвращает право на выполнение вызывающей стороне, и вызывающая сторона может продолжить выполнение функции в будущем. Этот метод планирования точно такой же, как функция генератора (Generator Function), поэтому, если вы используете функцию генератора, для реализации планировщика станет проще. Но команда React не реализовала это с функциями генератора, в основном потому, что функции генератора имеют состояние, а React хочет повторно выполнить задачу без состояния. Может относиться кофициальное объяснение.

Связь с MessageChannel

Так каковы отношения между планировщиком и MessageChannel?

Ключевой момент в том, что когдаscheduler.shouldYield()вернутьtrueПосле этого Scheduler должен соответствовать следующим функциональным пунктам:

  1. Приостановите выполнение JS, верните основной поток в браузер и дайте браузеру возможность обновить страницу.
  2. Продолжайте планировать задачи в какой-то момент в будущем и выполнять задачи, которые не были завершены в прошлый раз.

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

事件循环代码.png

Целью использования MessageChannel является создание задач макросов.. Код для использования MessageChannel в Планировщике выглядит следующим образом:

const channel = new MessageChannel()
const port = channel.port2

// 每次 port.postMessage() 调用就会添加一个宏任务
// 该宏任务为调用 scheduler.scheduleTask 方法
channel.port1.onmessage = scheduler.scheduleTask

const scheduler = {
  scheduleTask() {
    // 挑选一个任务并执行
    const task = pickTask()
    const continuousTask = task()

    // 如果当前任务未完成,则在下个宏任务继续执行
    if (continuousTask) {
      port.postMessage(null)
    }
  },
}

Почему бы не выбрать setTimeout(fn, 0)

setTimeout(fn, 0)Это наше наиболее часто используемое средство для создания макросов.Почему React не решил использовать его для реализации планировщика?

Причина является рекурсивным исполнениемsetTimeout(fn, 0), конечный интервал становится равным 4 миллисекундам вместо начальной 1 миллисекунды. В браузере можно выполнить следующий код:

var count = 0

var startVal = +new Date()
console.log("start time", 0, 0)
function func() {
  setTimeout(() => {
    console.log("exec time", ++count, +new Date() - startVal)
    if (count === 50) {
      return
    }
    func()
  }, 0)
}

func()

Текущий результат:setTimeout_0_throttle.png

При использованииsetTimeout(fn, 0)Внедрение планировщика приведет к потере 4 миллисекунд. Поскольку для 60 FPS требуется не более 16,66 мс между кадрами, 4 мс — это пустая трата времени, которую нельзя игнорировать.

Заинтересованные студенты могут попробоватьsetInterval(fn, 0), что дает тот же результат, что и setTimeout.

// setInterval 0ms 试试
var count = 0
var startVal = +new Date()
var timer = setInterval(() => {
  console.log("exec time", ++count, +new Date() - startVal)
  if (count >= 50) {
    clearInterval(timer)
  }
}, 0)

Почему бы не выбрать requestAnimationFrame(fn)

мы знаемrAF()вызывается перед обновлением страницы.

Если первое планирование задачи не выполненоrAF()Запущено, например, прямое исполнениеscheduler.scheduleTask(), то он будет выполнен один раз перед этим обновлением страницыrAF()Обратный вызов, который является второй задачей планирования. так что используйтеrAF()Реализация приведет к выполнению перед обновлением этой страницы.дваждыЗадача.

Почему дважды, а не три или четыре? Потому чтоrAF()вызывается снова в обратном вызовеrAF(), сделаю второйrAF()Обратный вызов выполняется перед следующим кадром, а не перед текущим кадром.

Еще одна причинаrAF()Интервал триггера неопределен. Если браузер обновляет страницу после интервала 10 мс, то 10 мс будут потрачены впустую.

Существующие WEB-технологии не определяют, что и когда браузер должен обновлять на странице, поэтому обычно считается, что после завершения задачи макроса браузер определяет, следует ли обновлять страницу в данный момент. Если страницу необходимо обновить, выполнитеrAF()обратный звонок и обновление страницы. В противном случае выполните следующую задачу макроса.事件循环代码.png

Суммировать

React Scheduler использует MessageChannel по следующим причинам:Генерировать макро-задачи,выполнить:

  1. Основной поток обратно в браузер, так что страница обновления браузера.
  2. Браузер продолжает выполнять незавершенные задачи после обновления страницы.

Почему бы не использовать микрозадачи?

  1. Все микрозадачи будут выполнены до обновления страницы, поэтому цель «возврата основного потока в браузер» не будет достигнута.

почему бы не использоватьsetTimeout(fn, 0)Шерстяная ткань?

  1. рекурсивныйsetTimeout()Вызов сделает интервал вызова 4 мс, что приведет к потере 4 мс.

почему бы не использоватьrAF()Шерстяная ткань?

  1. Если последнее расписание задач не былоrAF()Запущено, вызовет планирование двух задач перед обновлением текущего кадра.
  2. Время обновления страницы неизвестно. Если браузер обновляет страницу с интервалом в 10 мс, то эти 10 мс будут потрачены впустую.

Рекомендуются другие хорошие статьи React

  1. Оптимизация производительности React | Включая принципы, методы, демонстрацию, использование инструментов
  2. Расскажите о useSWR для повышения эффективности разработки, включая идеи дизайна useSWR, плюсы и минусы, а также передовой опыт.
  3. Почему React использует техническое решение Lane

Карьера

Автор находится вЧэнду-ByteDance-Направление частного облака, основной стек технологий — React + Node.js. Команда быстро расширяется, а техническая атмосфера внутри группы активна. Публичное облако и частное облако только появились, есть много технических проблем, и можно ожидать будущего.

Заинтересованные лица могут отправить свое резюме по этой ссылке:job.toutiao.com/s/e69g1rQ

Вы также можете добавить мой WeChatmoonball_cxy, общаться вместе, дружить.

Оригинальность непростая, не забудь поставить лайк и поощрить ❤️