Pull to refresh

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

Reading time10 min
Views88K

Каждый 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))

Вот тут хитрее. В момент выполнения executor'а регистрируется макротаска, при выполнении которой регистрируется микротаска.

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

Микрозадачи

Макрозадачи

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.

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

Микрозадачи

Макрозадачи

console.log(1)

console.log(2),0

console.log(3)

console.log(5)

console.log(4)

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

console.log(6)

console.log(7),0

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

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

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

Пруф
Пруф

1746 это некий случайный идентификатор. Он к нашему ответу отношения не имеет.
Когда что-то выполняешь вручную в консоли, результат последней синхронной операции записывается в консоль. Здесь последним был setTimeout, который вернул id созданного таймаута (тот самый, на который можно натравить clearTimeout).

(данный отрывок взял из комментов, спасибо аудитории).

ЗАДАЧА 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.
Ну ничего страшного, решим.

Также, в момент обновления статьи, я решил, немного дополнить "мой метод объяснения". Вроде бы я сам говорю, давайте на бумажке все распишем, а в итоге один пункт делаю в голове, а именно регистрацию в WEB.API. Возможно для простых задач и ок, но в сложных уже можно если не ошибиться, то не совсем верно объяснить, и как верно подметили в комментах, "натягивать" ответ на решение. Не надо так =).

1. setTimeout(() => console.log('in setTimeout1'), 1000 ) - уходит в Web.Api

WEB.API

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

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

Микрозадачи

Макрозадачи

Обращу внимание на то, что пока у нас задачи зарегистрированы в WEB.Api, они не переходят в наши очереди.

const myPromise = (delay) => new Promise((res, rej) => { setTimeout(res, delay) })
myPromise(1000).then(res => console.log('in Promise 1'))

Конструкция new Promise, выполнится сразу же при создании, а значит результатом, будет setTimeout, который сначала пойдет в Web.api, потом станет макротаской, которая в свою очередь породит микрозадачу.

Итого, myPromise(1000) возвращает setTimeout, который сначала надо зарегистрировать в Web.api.

WEB.API

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

res1, 1000

Заполним далее. Так как у нас все задачи это setTimeout (на верхнем уровне), то давайте заполним до конца web.Api

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'

WEB.API

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

res1, 1000

res2, 2000

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

res3, 1000

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

res4, 5000

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

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

Микрозадачи

Макрозадачи

'in setTimeout2' 😎

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

  • res1, 1000

  • res3, 1000

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

Все они переходят в очередь макрозадач.

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

Микрозадачи

Макрозадачи

'in setTimeout2' 😎

'in setTimeout1'

res1

res3

'in setTimeout4'

WEB.API

res2, 2000

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

res4, 5000

Макрозадачи выполняются по одной. Сначала уходит 'in setTimeout1';
Затем, наступает очередь res1, но помним, что внутри есть исполнения промиса, а это значит что она переходит в очередь микротасок и сразу же там исполняется (реальная очередь основного потока пуста, очередь микротасок пуста).
То же самое будет и с res3. А in setTimeout4' выполнится как обычная макрозадача.

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

Микрозадачи

Макрозадачи

'in setTimeout2' 😎

'in setTimeout1'😎

res1

res3

'in setTimeout4'

Да, и сейчас уже можно вспомнить, что res1 => console.log('in Promise 1');
Итого, получаем:

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

Микрозадачи

Макрозадачи

'in setTimeout2' 😎

'in setTimeout1'😎

'in Promise 1'😎

'in Promise 3'😎

'in setTimeout4'😎

Итак, у нас следующая партия задачек выходит из web.api

  • res2, 2000

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

Они обе сначала попадают в очередь макротасок.
Затем res2 (in Promise2) переходит в очередь микротасок, исполняется и затем исполняется 'in setTimeout3'.

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

Микрозадачи

Макрозадачи

'in setTimeout2' 😎

'in setTimeout1'😎

'in Promise 1'😎

'in Promise 3'😎

'in setTimeout4'😎

'in Promise 2'😎

'in setTimeout3'😎

И остается единственная задачка, оставшаяся в web.api

WEB.API

res4, 5000

Когда подходит время ее исполнения, она переходит в очередь макрозадач, тк она единственная во всем списке очередей (не считая исполненных, со смайликом), она выполняется. Переходит из макрозадач - в микрозадачи, и сразу же исполняется.

Итого, мы получаем уже и готовый ответ:

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

Микрозадачи

Макрозадачи

'in setTimeout2' 😎

'in setTimeout1'😎

'in Promise 1'😎

'in Promise 3'😎

'in setTimeout4'😎

'in Promise 2'😎

'in setTimeout3'😎

'in Promise'😎

Пруф
Пруф

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

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

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

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

Tags:
Hubs:
Total votes 20: ↑19 and ↓1+18
Comments24

Articles