Моему 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 не выполняется по порядку. И каким-то образом события пользовательского интерфейса имеют приоритет. Это потрясающе. Но как это работает? И могу ли я сам определять приоритеты событий?
Вот песочница, где вы можете поиграть с примером.