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

Каждый JS-разработчик, или тот, кто хочет им стать, сталкивался или на собеседованиях, или на разборах собесов про задачки на событийный цикл. Сначала интервьюер спрашивает кратко про event loop, затем показывает кусок кода, где обычно есть несколько console.log (), и нас просят сказать очередность появления логов. Далее, дается ответ, и если он правильный, идут дальше, а если нет, интервьюер скажет свою последовательность (возможно даст небольшой комментарий) и также двинутся дальше. Очень редко, если вы ошиблись, вам подробно объяснят, что как и почему. Все-таки — собеседование.

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

Event loop

Лучше, чем на https://learn.javascript.ru/event-loop теорию я не объясню, так что давайте сразу перейдем к задачкам.

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

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

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

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

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

  3. Выполняется макротаска
    Макротаска — это у нас или браузерное API, или манипуляции с DOM деревом (дополните меня в комментариях, пожалуйста)

    Далее, цикл повторяется.
    Если основной поток все и микрозадач тоже нет, последовательно выполняются макротаски.

Как я предлагаю решать задачи на event loop

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

Не стесняйтесь на собеседованиях использовать ее, лучше даже от руки, на бумажке.
Решение в голове, в стрессовой ситуации — большой шанс совершить ошибку.

Основной поток

Микрозадачи

Макрозадачи

Заполняйте табличку так, чтобы каждый скрипт, был на отдельной строке! это важно.
Ну, давайте приступим. У нас есть простенькая задачка, уровня Junior.

ЗАДАЧА 1

setTimeout(function timeout() {
console.log('Таймаут');
}, 0);

let p = new Promise(function(resolve, reject) {
console.log('Создание промиса');
resolve();
});

p.then(function(){
console.log('Обработка промиса');
});

console.log('Конец скрипта');

Идем сверху-вниз, именно так, как это делает парсер нашего кода.

setTimeout(function timeout() { console.log('Таймаут'); }, 0);

Сначала, видим setTimeout, это макрозадача (браузерное API), и мы должны его зарегистрировать (если не понятно что такое регистрация, предлагаю посмотреть это видео).

Помимо занесения результата выполнения скрипта в нашу табличку, укажем и время, через которое он должен сработать (время не точно, но гарантирующее задержку, то есть он сработает, не раньше, чем через N секунд).

Основной поток

Микрозадачи

Макрозадачи

'Таймаут',0

let p = new Promise(function(resolve, reject)
{ console.log('Создание промиса');
resolve(); });

Заметим, что здесь у нас создается промис, console.log('Создание промиса') выполнится, т.к. это по сути основной поток, нам не важно, как завершится промис.

Основной поток

Микрозадачи

Макрозадачи

'Таймаут',0

'Создание промиса'

p.then(function(){ console.log('Обработка промиса'); });

А тут, мы видим, что наш промис уже исполняется, видим цепочку. Следовательно, это микрозадача.

Основной поток

Микрозадачи

Макрозадачи

'Таймаут',0

'Создание промиса'

'Обработка промиса'

И финальное, console.log('Конец скрипта');
Это основной поток.

Основной поток

Микрозадачи

Макрозадачи

'Таймаут',0

'Создание промиса'

'Обработка промиса'

'Конец скрипта'

А дальше, остается самое простое и веселое. Собрать наш ответ, как бургер, по методичке.

Идем по нашему гайду:

  1. Основной поток (все задачи)

  2. Микрозадачи (все задачи)

  3. Макрозадача

  4. Repeat, please.

Итак, у нас получается

  1. 'Создание промиса'

  2. 'Конец скрипта'

  3. 'Обработка промиса'

  4. 'Таймаут'

ПруфыПруфы

ЗАДАЧА 2

Окей, давайте сразу решим вторую задачку, посложнее.

console.log(1);

setTimeout(() => console.log(2));

Promise.resolve().then(() => console.log(3));

Promise.resolve().then(() => setTimeout(() => console.log(4)));

Promise.resolve().then(() => console.log(5));

setTimeout(() => console.log(6));

console.log(7);

console.log(1) — основной поток выполнения кода

Основной поток

Микрозадачи

Макрозадачи

console.log(1)

setTimeout(() => console.log(2)) — регистрируем макрозадачу в браузерное API, с нулевым сроком срабатывания

Основной поток

Микрозадачи

Макрозадачи

console.log(1)

console.log(2), 0

Promise.resolve().then(() => console.log(3)) — микрозадача

Основной поток

Микрозадачи

Макрозадачи

console.log(1)

console.log(2), 0

console.log(3)

Promise.resolve().then(() => setTimeout(() => console.log(4)))

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

Основной поток

Микрозадачи

Макрозадачи

console.log(1)

console.log(2), 0

console.log(3)

console.log(4),0 =>

Promise.resolve().then(() => console.log(5)) — микротаска

Основной поток

Микрозадачи

Макрозадачи

console.log(1)

console.log(2), 0

console.log(3)

console.log(4),0 =>

console.log(5)

setTimeout(() => console.log(6)) — макрозадача с нулевой отсрочкой срабатывания.

Основной поток

Микрозадачи

Макрозадачи

console.log(1)

console.log(2), 0

console.log(3)

console.log(4),0 =>

console.log(5)

console.log(6), 0

И финальная, console.log(7) — основной поток.

Основной поток

Микрозадачи

Макрозадачи

console.log(1)

console.log(2), 0

console.log(3)

console.log(4),0 =>

console.log(5)

console.log(6), 0

console.log(7)

Фух, заполнили. Давайте собирать наш ответ. Опять покажу наш гайд:

  1. Основной поток (все задачи)

  2. Микрозадачи (все задачи)

  3. Макрозадача

  4. Repeat, please.

Итак, у нас получается

  1. console.log(1)

  2. console.log(7)

  3. console.log(3)

  4. console.log(5)

  5. console.log(2)

  6. console.log(6)

  7. console.log(4)

Тут нужно учесть, что console.log(4),0 встанет в конец очереди макрозадач. А там у нас уже находятся 2, 6, поэтому 4 идет в конец.

ПруфПруф

ЗАДАЧА 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);

Прошу обратить внимание, на некоторую разницу в том, как записаны промисы, и будьте уверены, они так тоже прекрасно срабатывают и значения передадутся в функции console.log

Самый интересный кусочек здесь, это

new Promise(resolve => setTimeout(resolve)).then(()=>console.log(4))

Важно помнить!

Функция, переданная в конструкцию new Promise, называется исполнитель (executor). Когда Promise создаётся, она запускается автоматически.

Я уже пропущу подробное заполнение таблицы, надеюсь оно понятно.

console.log(1);

setTimeout(() => console.log(2));

Promise.reject(3).catch(console.log);

new Promise(resolve => setTimeout(resolve)).then(()=>console.log(4))

Это промис, который запустит порождает макрозадачу, порождающую микрозадачу. Вот так хитро получилось. Пока в табличку поместим вот так.

Основной поток

Микрозадачи

Макрозадачи

console.log(1)

console.log(2),0

console.log(3)

setTimeout(resolve)).then(()=>console.log(4))

Ну и добьем нашу табличку, и будем собирать ответ.

solve(5).then(console.log);

console.log(6);

setTimeout(() => console.log(7),0);

Основной поток

Макрозадачи

Микрозадачи

console.log(1)

console.log(2),0

console.log(3)

setTimeout(resolve)).then(()=>console.log(4)) =>

console.log(5)

console.log(6)

console.log(7),0

Ну что, надеюсь пока все понятно. Начинаем аккуратно собирать ответ.

Опять же наш гайд. Надеюсь вы его уже выучили.

  1. Основной поток (все задачи)

  2. Микрозадачи (все задачи)

  3. Макрозадача

  4. Repeat, please.

Итак, у нас получается 1 6 3

Тут все ясно понятно. А вот что с четверкой? Микрозадача, порождает макрозадачу, обновим нашу табличку:

Основной поток

Микрозадачи

Макрозадачи

console.log (1)

console.log(2),0

console.log (3)

setTimeout(resolve)).then(()=>console.log(4))

console.log(5)

console.log (6)

console.log(7),0

Получается в момент выполнения микротасок, мы регистрируем макрозадачу, которая потом выполнит микрозадачу. Идем дальше.

Наш ответ 1 6 3 5 2 …

Опять прервемся. Когда очередь подходит к нашей хитрой четверке, то вспоминаем, что макрозадачи срабатывают не все, а лишь по одной, а потом цикл проверяется очередь основного потока и микрозадач. А у нас получается, что макрозадача порождает микрозадачу, выполняет ее, и лишь потом берет следующую макрозадачу. Поэтому можно еще раз перерисовать табличку.

Основной поток

Микрозадачи

Макрозадачи

console.log (1)

console.log (2),0

console.log (3)

console.log (5)

console.log(4)

console.log (6)

console.log(7),0

Итого, наш ответ 1 6 3 5 2 4 7

ПруфПруф

1746 это некий случайный идентификатор. Он к нашему ответу отношения не имеет.
Если кто может более точно объяснить почему он тут появляется, прошу в комментарии. У меня как-то кургузо получается, поэтому писать не буду.

ЗАДАЧА 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 '));

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

У нас чередуются классические макротаски с созданными, через функцию myPromise.
Ну ничего страшного, решим.

setTimeout(() => console.log('in setTimeout1'), 1000)

Основной поток

Микрозадачи

Макрозадачи

'in setTimeout1', 1000

myPromise(1000).then(res => console.log('in Promise 1'))
Напомню, функцию возвращает промис, который породит макрозадачу. Первоначально идет в очередь микрозадач.

Основной поток

Микрозадачи

Макрозадачи

'in setTimeout1', 1000

setTimeout(console.log('in Promise 1'), 1000)

setTimeout(() => console.log('in setTimeout2'), 100)

Основной поток

Микрозадачи

Макрозадачи

'in setTimeout1', 1000

setTimeout(console.log('in Promise 1'), 1000)

'in setTimeout2',100

myPromise(2000).then(res => console.log('in Promise 2'))

Основной поток

Микрозадачи

Макрозадачи

'in setTimeout1', 1000

setTimeout(console.log('in Promise 1'), 1000)

'in setTimeout2',100

setTimeout(console.log('in Promise 2'), 2000)

setTimeout(() => console.log('in setTimeout3'), 2000)

Основной поток

Микрозадачи

Макрозадачи

'in setTimeout1', 1000

setTimeout(console.log('in Promise 1'), 1000)

'in setTimeout2',100

setTimeout(console.log('in Promise 2'), 2000)

'in setTimeout3',2000

myPromise(1000).then(res => console.log('in Promise 3'))

Основной поток

Микрозадачи

Макрозадачи

'in setTimeout1', 1000

setTimeout(console.log('in Promise 1'), 1000)

'in setTimeout2',100

setTimeout(console.log('in Promise 2'), 2000)

'in setTimeout3',2000

setTimeout(console.log('in Promise 3'), 1000)

Я думаю принцип понятен, и я добью крайние две строчки одной табличкой

setTimeout(() => console.log('in setTimeout4'), 1000)

myPromise (5000).then (res => console.log ('in Promise '))

Основной поток

Микрозадачи

Макрозадачи

'in setTimeout1', 1000

setTimeout(console.log('in Promise 1'), 1000)

'in setTimeout2',100

setTimeout(console.log('in Promise 2'), 2000)

'in setTimeout3',2000

setTimeout(console.log('in Promise 3'), 1000)

'in setTimeout4',1000

setTimeout(console.log('in Promise'), 5000)

Так, ну что, давайте аккуратненько начнем собирать наш ответ.

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

Основной поток

Микрозадачи

Макрозадачи

'in setTimeout1', 1000

'in Promise 1', 1000

'in setTimeout2',100

'in Promise 2', 2000

'in setTimeout3',2000

'in Promise 3', 1000

'in setTimeout4',1000

'in Promise', 5000

А теперь, внимательно:

Сначала сработает тот, у которого меньше всего время ожидания (100 мс), затем пойдут с 1000 мс (в порядке очереди) и так далее.

'in setTimeout2'
'in setTimeout1'
'in Promise 1'
'in Promise 3'
'in setTimeout4'
'in Promise 2'
'in setTimeout3'
'in Promise'

ПруфПруф


Итого, я очень надеюсь, что подход с табличкой будет полезным для junior+, middle разработчиков при прохождении собеседований.

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

P.S. кто знает как таблицы копировать?, а то я вручную их все заполнял… Что-то не разобрался.

© Habrahabr.ru