Event Loop
Event loop
Задачи на собеседованиях. Event loop. JS
Принцип решения задач на Event loop
Основное принцип в решении задачек на событийный цикл.
Выполняется основной поток кода (+ выполняются скрипты в теле создания промисов)
Выполняются микротаски
По факту, микротаски = промисы.
Также есть возможность принудительно микромизировать задачу с помощьюqueueMicrotask(f)
.
(важно помнить, что исполняются ВСЕ промисы, и нужно об этом помнить, так как по факту, так можно застопорить процесс выполнения скриптов и очень не скоро приступить к макротаскам)Выполняется макротаска
Макротаска - это у нас или браузерное API, или манипуляции с DOM деревом (дополните меня в комментариях, пожалуйста)Далее, цикл повторяется.
Если основной поток все и микрозадач тоже нет, последовательно выполняются макротаски.
Как я предлагаю решать задачи на event loop
В ходе решения задачек, я пришел к выводу, что можно использовать вот такую табличку, и с ее помощью неплохо упрощать себе жизнь.
Не стесняйтесь на собеседованиях использовать ее, лучше даже от руки, на бумажке.
Решение в голове, в стрессовой ситуации - большой шанс совершить ошибку.
Основной поток | Микрозадачи | Макрозадачи |
---|---|---|
Заполняйте табличку так, чтобы каждый скрипт, был на отдельной строке! это важно. |
Задачи
ЗАДАЧА 3
console.log(1);
setTimeout(() => console.log(2));
Promise.reject(3).catch(console.log);
new Promise(resolve => setTimeout(resolve)).then(() => console.log(4));
Promise.resolve(5).then(console.log);
console.log(6);
setTimeout(() => console.log(7),0);
А вот что с четверкой? Макрозадача, порождает микрозадачу.
Когда очередь подходит к нашей хитрой четверке, то вспоминаем, что макрозадачи срабатывают не все, а лишь по одной, а потом цикл проверяется очередь основного потока и микрозадач. А у нас получается, что макрозадача порождает микрозадачу, выполняет ее, и лишь потом берет следующую макрозадачу.
Итого, наш ответ 1 6 3 5 2 4 7
ЗАДАЧА 4
const myPromise = (delay) => new Promise((res, rej) => { setTimeout(res, delay) })
setTimeout(() => console.log('in setTimeout1'), 1000);
myPromise(1000).then(res => console.log('in Promise 1'));
setTimeout(() => console.log('in setTimeout2'), 100);
myPromise(2000).then(res => console.log('in Promise 2'));
setTimeout(() => console.log('in setTimeout3'), 2000);
myPromise(1000).then(res => console.log('in Promise 3'));
setTimeout(() => console.log('in setTimeout4'), 1000);
myPromise(5000).then(res => console.log('in Promise '));
res1...4 это функции, которые внутри себя содержат микротаски, но после web.api попадут сначала в очередь макротасок.
WEB.API |
---|
() => console.log('in setTimeout1'), 1000 |
res1, 1000 |
() => console.log('in setTimeout2'), 100 |
res2, 2000 |
() => console.log('in setTimeout3'), 2000 |
res3, 1000 |
() => console.log('in setTimeout4'), 1000 |
res4, 5000 |
Итак, все задачки находятся в web.api. Давайте начнем их исполнять. Первой уходит та, у которой закончилось время простоя, это 'in setTimeout2'
setTimeout(() => console.log('in setTimeout2'), 100)
Она переходит в очередь макрозадач, и так как очередь микротасок и основного потока - пусты, исполняется.
Основной поток | Микрозадачи | Макрозадачи |
---|---|---|
'in setTimeout2' |
По факту, исполнение макрозадачи, в нашей системе табличек, можно назвать переходом в основной поток. Так, как у нас посреди решения нашей задачи, нового ничего не приходит, я думаю так сделать законно. Только помним, она уже выполнена. И очередь пуста. Мы это делаем только для того, чтобы потом легко собрать ответ. Давайте пометим ее неким символом, чтобы помнить, что задача уже выполнена.
Основной поток | Микрозадачи | Макрозадачи |
---|---|---|
'in setTimeout2' 😎 |
- () => console.log('in setTimeout1'), 1000
- res1, 1000
- res3, 1000
- () => console.log('in setTimeout4'), 1000
Все они переходят в очередь макрозадач.
Основной поток | Микрозадачи | Макрозадачи |
---|---|---|
'in setTimeout2' 😎 | ||
'in setTimeout1' | ||
res1 | ||
res3 | ||
'in setTimeout4' | ||
Макрозадачи выполняются по одной. Сначала уходит 'in setTimeout1'; Затем, наступает очередь res1, но помним, что внутри есть исполнения промиса, а это значит что она переходит в очередь микротасок и сразу же там исполняется (реальная очередь основного потока пуста, очередь микротасок пуста). | ||
То же самое будет и с res3. А 'in setTimeout4' выполнится как обычная макрозадача. |
Да, и сейчас уже можно вспомнить, что res1 => console.log('in Promise 1');
Итого, получаем:
Основной поток | Микрозадачи | Макрозадачи |
---|---|---|
'in setTimeout2' 😎 | ||
'in setTimeout1' 😎 | ||
'in Promise 1' 😎 | ||
'in Promise 3' 😎 | ||
'in setTimeout 4' 😎 |
Итого, мы получаем уже и готовый ответ:
Основной поток | Микрозадачи | Макрозадачи |
---|---|---|
'in setTimeout2' 😎 | ||
'in setTimeout1' 😎 | ||
'in Promise 1' 😎 | ||
'in Promise 3' 😎 | ||
'in setTimeout 4' 😎 | ||
'in Promise 2' 😎 | ||
'in setTimeout3' 😎 | ||
'in Promise' 😎 |
Также, в комментариях, один наш коллега, дал очень классную гифку, которая показывает решение этой задачи. Немного велика скорость, но думаю в сумме с объяснениями выше, сомнений больше быть не должно.
Событийный цикл: микрозадачи и макрозадачи
Макрозадачи и Микрозадачи
Помимо макрозадач, описанных в этой части, существуют микрозадачи, упомянутые в главе Микрозадачи.
Микрозадачи приходят только из кода. Обычно они создаются промисами: выполнение обработчика .then/catch/finally
становится микрозадачей. Микрозадачи также используются «под капотом» await
, т.к. это форма обработки промиса.
Также есть специальная функция queueMicrotask(func)
, которая помещает func
в очередь микрозадач.
Сразу после каждой макрозадачи движок исполняет все задачи из очереди микрозадач перед тем, как выполнить следующую макрозадачу или отобразить изменения на странице, или сделать что-то ещё.
Все микрозадачи завершаются до обработки каких-либо событий или рендеринга, или перехода к другой макрозадаче.
Макрозадачи срабатывают не все, а лишь по одной, а потом цикл проверяется очередь основного потока и микрозадач
Это важно, так как гарантирует, что общее окружение остаётся одним и тем же между микрозадачами – не изменены координаты мыши, не получены новые данные по сети и т.п.
Если мы хотим запустить функцию асинхронно (после текущего кода), но до отображения изменений и до новых событий, то можем запланировать это через queueMicrotask
.
Итого
Более подробный алгоритм событийного цикла (хоть и упрощённый в сравнении со https://html.spec.whatwg.org/multipage/webappapis.html > event-loop-processing-model):
- Выбрать и исполнить старейшую задачу из очереди макрозадач (например, «script»).
- Исполнить все микрозадачи:
- Пока очередь микрозадач не пуста: - Выбрать из очереди и исполнить старейшую микрозадачу
- Отрисовать изменения страницы, если они есть.
- Если очередь макрозадач пуста – подождать, пока появится макрозадача.
- Перейти к шагу 1.
Чтобы добавить в очередь новую макрозадачу:
- Используйте
setTimeout(f)
с нулевой задержкой.
Этот способ можно использовать для разбиения больших вычислительных задач на части, чтобы браузер мог реагировать на пользовательские события и показывать прогресс выполнения этих частей.
Также это используется в обработчиках событий для отложенного выполнения действия после того, как событие полностью обработано (всплытие завершено).
Для добавления в очередь новой микрозадачи:
- Используйте
queueMicrotask(f)
. - Также обработчики промисов выполняются в рамках очереди микрозадач.
События пользовательского интерфейса и сетевые события в промежутках между микрозадачами не обрабатываются: микрозадачи исполняются непрерывно одна за другой.
Поэтому queueMicrotask
можно использовать для асинхронного выполнения функции в том же состоянии окружения.