JavaScript / 37 минут
Event loop и конкурентность
Проследите стек вызовов, задачи, микрозадачи и порядок асинхронных действий.
- Проследите стек вызовов, задачи, микрозадачи и порядок асинхронных действий.
- Применить тему в задаче «Event loop и конкурентность» и объяснить граничные случаи.
Около 7 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.
Стек и выполнение до завершения
Когда вызывается функция, среда хранит информацию о её выполнении в стеке вызовов. Вложенный вызов добавляет новый кадр, возврат убирает его. Обычная синхронная часть задачи выполняется до освобождения стека: таймер не прерывает её посередине только потому, что прошёл указанный интервал. Долгий цикл поэтому способен заморозить интерфейс даже в программе с большим количеством async-функций.
Async — способ организовать продолжения, а не команда сделать любую тяжёлую работу бесплатной. Вычисление миллиона элементов перед первым await остаётся обычной синхронной работой. Для отдельного потока браузер предоставляет Worker, а для небольших порций интерфейсной работы иногда нужно осознанно уступать управление между частями. Выбор зависит от стоимости вычисления и требований к отзывчивости.
Задачи и API среды
Событие, callback таймера и другие действия среды могут ставить работу в очереди задач. Event loop выбирает следующую доступную задачу, когда текущая завершилась и выполнены соответствующие промежуточные стадии. Значение задержки setTimeout — не точное обещание времени запуска. На него влияют занятость потока, ограничения среды и другие ожидающие действия.
console.log('A');
setTimeout(() => console.log('timer'), 0);
console.log('B');A B timer
Нулевой таймер означает отложенную возможность выполнить callback, а не немедленный вызов. Если после регистрации таймера выполняется длительный синхронный расчёт, callback подождёт. Это объясняет, почему таймер нельзя использовать как точные часы и почему UI-эффект может появиться заметно позже ожидаемой задержки.
Очередь микрозадач
Продолжения Promise и queueMicrotask используют микрозадачи. После завершения текущей синхронной работы среда обрабатывает их до перехода к следующей обычной задаче в соответствующем цикле. Порядок регистрации микрозадач имеет значение. Микрозадачи, созданные в процессе обработки, тоже требуют внимания перед возвратом к обычным задачам.
console.log('start');
Promise.resolve().then(() => console.log('promise'));
queueMicrotask(() => console.log('microtask'));
setTimeout(() => console.log('timer'), 0);
console.log('end');start end promise microtask timer
Здесь Promise уже завершён, но callback then всё равно не вызывается как обычная синхронная функция. Это важное отличие от executor конструктора Promise, который запускается сразу. Не делайте вывод о времени callback только по тому, что результат операции уже известен.
await и точка продолжения
До await async-функция выполняется как обычный синхронный вызов. После ожидания продолжение планируется через механизм Promise. Даже ожидание уже доступного значения меняет точку продолжения относительно синхронного кода вызывающей функции. Поэтому полезно разбирать пример по отдельным записям в журнал, а не угадывать весь порядок по наличию слова async.
async function run() {
console.log('inside before');
await 1;
console.log('inside after');
}
run();
console.log('outside');inside before outside inside after
Это не означает, что await всегда даёт браузеру возможность немедленно нарисовать новый кадр. Обработка микрозадач и возможность отрисовки — разные стадии. Если бесконечно добавлять новые микрозадачи, до следующей задачи или кадра можно так и не дойти.
Конкурентность, параллелизм и гонки
Конкурентность означает, что программа имеет несколько незавершённых операций и организует их продолжения. Параллелизм означает фактическое одновременное выполнение работы на разных ресурсах. JavaScript-события могут приводить к гонкам состояния и без параллельных инструкций: старый HTTP-запрос завершился позже нового и перезаписал актуальный результат поиска.
Решение такой гонки — не переставить случайный таймер, а определить владельца результата. Используют отмену ненужного запроса, идентификатор актуальной операции или проверку состояния перед применением ответа. Тайминговые случайности не должны решать, какой результат считается последним. В тестах полезнее управлять порядком завершения, чем ждать произвольное число миллисекунд.
Практическое восстановление порядка
Задание записывает синхронную метку, Promise callback, queueMicrotask и таймер. Сначала нарисуйте две очереди и отметьте текущий стек. Затем добавляйте записи по мере регистрации, а выполняйте — по правилам соответствующей стадии. Возвращать журнал нужно после того, как таймер добавил последнюю запись, иначе проверка увидит незавершённый результат.
Не используйте фиксированную строку ожидаемого ответа вместо реальных механизмов: практика нужна для проверки собственного понимания очередей. В реальном приложении обращайте внимание на незавершённые действия после размонтирования интерфейса и на таймеры без владельца. Явная отмена и очистка помогают и корректности, и отзывчивости программы.