Event Loop

Event loop

Задачи на собеседованиях. Event loop. JS

Принцип решения задач на Event loop

Основное принцип в решении задачек на событийный цикл.

  1. Выполняется основной поток кода (+ выполняются скрипты в теле создания промисов)

  2. Выполняются микротаски
    По факту, микротаски = промисы.
    Также есть возможность принудительно микромизировать задачу с помощью queueMicrotask(f).
    (важно помнить, что исполняются ВСЕ промисы, и нужно об этом помнить, так как по факту, так можно застопорить процесс выполнения скриптов и очень не скоро приступить к макротаскам)

  3. Выполняется макротаска
    Макротаска - это у нас или браузерное 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'😎

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

attachments/69e04ab26996f6104f28bcd51a87f3d3.gif

Событийный цикл: микрозадачи и макрозадачи

Макрозадачи и Микрозадачи

Помимо макрозадач, описанных в этой части, существуют микрозадачи, упомянутые в главе Микрозадачи.

Микрозадачи приходят только из кода. Обычно они создаются промисами: выполнение обработчика .then/catch/finally становится микрозадачей. Микрозадачи также используются «под капотом» await, т.к. это форма обработки промиса.

Также есть специальная функция queueMicrotask(func), которая помещает func в очередь микрозадач.

Сразу после каждой макрозадачи движок исполняет все задачи из очереди микрозадач перед тем, как выполнить следующую макрозадачу или отобразить изменения на странице, или сделать что-то ещё.

Все микрозадачи завершаются до обработки каких-либо событий или рендеринга, или перехода к другой макрозадаче.

Макрозадачи срабатывают не все, а лишь по одной, а потом цикл проверяется очередь основного потока и микрозадач

Это важно, так как гарантирует, что общее окружение остаётся одним и тем же между микрозадачами – не изменены координаты мыши, не получены новые данные по сети и т.п.

Если мы хотим запустить функцию асинхронно (после текущего кода), но до отображения изменений и до новых событий, то можем запланировать это через queueMicrotask.

Итого

Более подробный алгоритм событийного цикла (хоть и упрощённый в сравнении со https://html.spec.whatwg.org/multipage/webappapis.html > event-loop-processing-model):

  1. Выбрать и исполнить старейшую задачу из очереди макрозадач (например, «script»).
  2. Исполнить все микрозадачи:
    • Пока очередь микрозадач не пуста: - Выбрать из очереди и исполнить старейшую микрозадачу
  3. Отрисовать изменения страницы, если они есть.
  4. Если очередь макрозадач пуста – подождать, пока появится макрозадача.
  5. Перейти к шагу 1.

Чтобы добавить в очередь новую макрозадачу:

  • Используйте setTimeout(f) с нулевой задержкой.

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

Также это используется в обработчиках событий для отложенного выполнения действия после того, как событие полностью обработано (всплытие завершено).

Для добавления в очередь новой микрозадачи:

  • Используйте queueMicrotask(f).
  • Также обработчики промисов выполняются в рамках очереди микрозадач.

События пользовательского интерфейса и сетевые события в промежутках между микрозадачами не обрабатываются: микрозадачи исполняются непрерывно одна за другой.

Поэтому queueMicrotask можно использовать для асинхронного выполнения функции в том же состоянии окружения.