Javascript: избегайте нетерпеливого выполнения asyn c и отдавайте приоритет вызовам в цикле событий - PullRequest
0 голосов
/ 06 мая 2020

Моему Javascript приложению необходимо выполнить некоторые тяжелые вычисления в потоке пользовательского интерфейса (давайте проигнорируем веб-воркеров в рамках этого вопроса).
К счастью, работа, которую предстоит выполнить, до стыда параллельна и может быть разрезана на тысячи небольших асинхронных c функций.

То, что я хотел бы сделать, это: поставить в очередь все эти асинхронные c функции для события l oop, но приоритизировать события пользовательского интерфейса.
Если это работает , влияние на отзывчивость пользовательского интерфейса должно быть очень низким. Каждая из функций занимает всего несколько миллисекунд.

Проблема в том, что Javascript нетерпеливо выполняет asyn c код. Пока вы не столкнетесь с чем-то вроде await IO, он просто выполняет весь код, как если бы он был синхронным.

Я хотел бы разбить это: когда выполнение встречает вызов asyn c, он должен поместить его на событии l oop, но перед его выполнением он должен проверить, есть ли событие пользовательского интерфейса, ожидающее обработки.
По сути, я ищу способ немедленно yield из моих рабочих функций, не возвращая результат.

Я экспериментировал и нашел обходной путь. Вместо прямого вызова функции asyn c ее можно заключить в тайм-аут. Это в значительной степени именно то, что я хотел: вызов asyn c не выполняется, а ставится в очередь на событие l oop.
Вот простой пример, основанный на реакции. После нажатия кнопка обновится немедленно, вместо того, чтобы ждать, пока все вычисления завершатся sh.
(Пример, вероятно, выглядит как ужасный код, я помещаю тяжелые вычисления в функцию рендеринга. Но это просто для того, чтобы На самом деле не имеет значения, где запускаются функции asyn c, они всегда блокируют единственный поток пользовательского интерфейса):

// timed it, on my system it takes a few milliseconds
async function expensiveCalc(n: number) {
  for (let i = 0; i < 100000; i++) {
    let a = i * i;
  }
  console.log(n);
}

const App: React.FC = () => {
  const [id, setId] = useState("click me");

  // many inexpensive calls that add up
  for (let i = 0; i < 1000; i++) {
    setTimeout(() => expensiveCalc(i), 1);
  }

  // this needs to be pushed out as fast as possible
  return (
    <IonApp>
      <IonButton
        onClick={() => setId(v4())}>
        {id}
      </IonButton>
    </IonApp>
  );
};

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

Я не понимаю: это работает, даже если вы нажмете кнопку много раз! Я ожидал, что все вызовы события l oop будут обрабатываться одинаково. Итак, в первый раз рендеринг должен быть мгновенным, во второй раз нужно дождаться, пока все таймауты будут выполнены первым.
Но я вижу:

  • нажмите кнопку -> текст обновлен
  • немедленно нажмите кнопку еще раз -> текст обновлен
  • посмотрите журналы консоли: он считает до 1000, затем снова начинается с 0 и снова до 1000.

Таким образом, событие l oop не выполняется по порядку. И каким-то образом события пользовательского интерфейса имеют приоритет. Это потрясающе. Но как это работает? И могу ли я сам определять приоритеты событий?

Вот песочница, где вы можете поиграть с примером.

Ответы [ 2 ]

3 голосов
/ 07 мая 2020

Ответ Берги дает хорошее объяснение того, что происходит (в браузере event-l oop есть приоритеты задач).

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

Но я должен отметить, что то, что вы готовы сделать, поначалу кажется точным заданием requestIdleCallback было разработано для: выполнять обратный вызов только тогда, когда в событии ничего не происходит-l oop .

btn.onclick = (e) => {
  for (let i = 0; i<100000; i++) {
    requestIdleCallback( () => {
      log.textContent = 'executed #' + i;
    });
  }
  console.log( 'new batch' );
}
щелкните меня

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



1. Этот API уже можно протестировать в Chrome, включив chrome: // flags / # enable-experimental-web-platform-features

2 голосов
/ 06 мая 2020

Таким образом, событие l oop не выполняется по порядку. И каким-то образом события пользовательского интерфейса имеют приоритет. Это потрясающе. Но как это работает?

Событие браузера l oop - сложный зверь. Он действительно содержит несколько «очередей задач» для разных «источников задач», и браузеры могут свободно оптимизировать планирование между ними. Тем не менее, каждая очередь сама по себе подчиняется порядку. л oop.

...