На собеседовании тебе показывают 10 строк с setTimeout, Promise.then и console.log и спрашивают: в каком порядке выведет консоль? Без понимания Event Loop — загадка. С пониманием — задача решается по алгоритму за 30 секунд.
.then(), .catch(), .finally() — источники микрозадачawait под капотом создаёт микрозадачуКогда вы вызываете функцию, она добавляется на вершину стека. Когда завершается — удаляется:
function greet(name) {
return `Привет, ${name}!`
}
function main() {
const msg = greet('Иван') // greet попадает в стек
console.log(msg) // console.log попадает в стек
}
main()
// Стек: [main] → [main, greet] → [main] → [main, console.log] → []Макрозадачи попадают в Task Queue (очередь задач):
setTimeout(fn, delay)setInterval(fn, delay)setTimeout(() => console.log('Макрозадача'), 0)
// Даже с задержкой 0 — выполнится ПОСЛЕ всего синхронного кодаМикрозадачи попадают в Microtask Queue:
Promise.then, Promise.catch, Promise.finallyqueueMicrotask(fn)MutationObserverPromise.resolve().then(() => console.log('Микрозадача'))
// Выполнится после синхронного кода, но ПЕРЕД setTimeout1. Выполнить весь синхронный код (пока стек не опустеет)
2. Выполнить все микрозадачи из Microtask Queue (до опустошения)
3. Выполнить одну макрозадачу из Task Queue
4. Снова выполнить все микрозадачи
5. Повторить с шага 3
console.log('1 — синхронный')
setTimeout(() => console.log('4 — макрозадача'), 0)
Promise.resolve()
.then(() => console.log('2 — микрозадача'))
.then(() => console.log('3 — вторая микрозадача'))
console.log('После Promise.resolve — синхронный')
// Вывод:
// 1 — синхронный
// После Promise.resolve — синхронный
// 2 — микрозадача
// 3 — вторая микрозадача
// 4 — макрозадачаХотя setTimeout(fn, 0) означает "выполни как можно скорее", он всё равно помещается в Task Queue (макрозадачи). После выполнения синхронного кода Event Loop сначала опустошает очередь микрозадач, и только потом берёт одну макрозадачу.
queueMicrotask(fn) позволяет добавить функцию в очередь микрозадач напрямую, без создания Promise:
console.log('начало')
queueMicrotask(() => console.log('микрозадача'))
console.log('конец')
// начало → конец → микрозадачаconsole.log('A')
setTimeout(() => console.log('B'), 0)
Promise.resolve()
.then(() => {
console.log('C')
setTimeout(() => console.log('D'), 0)
})
.then(() => console.log('E'))
queueMicrotask(() => console.log('F'))
console.log('G')
// Порядок: A, G, C, F, E, B, D
// A, G — синхронный код
// C — первая .then() (микрозадача)
// F — queueMicrotask (добавлена до .then, но обе в очереди)
// E — вторая .then() (добавлена внутри C)
// B — первый setTimeout (макрозадача)
// D — второй setTimeout (добавлен внутри C, следующая макрозадача)1. Ожидание, что setTimeout(fn, 0) выполнится "немедленно":
let data = null
setTimeout(() => { data = 'загружено' }, 0)
console.log(data) // null — setTimeout ещё не выполнился!
// Правило: код после setTimeout выполняется ДО колбэка,
// даже с задержкой 02. Бесконечный цикл микрозадач блокирует рендеринг:
// Плохо: рекурсивные Promise бесконечно добавляют микрозадачи,
// браузер не может отрисовать следующий кадр
function badLoop() {
Promise.resolve().then(badLoop) // бесконечно добавляет микрозадачи
}
badLoop()
// Хорошо: используй requestAnimationFrame или setTimeout для периодических задач
function goodLoop() {
setTimeout(goodLoop, 0) // макрозадача — браузер может отрисовать кадр между итерациями
}3. async/await создаёт микрозадачи в неочевидных местах:
async function main() {
console.log('1')
await Promise.resolve() // await = .then = микрозадача
console.log('3') // выполнится после синхронного кода ВЫЗЫВАЮЩЕГО
}
main()
console.log('2') // выполнится ДО console.log('3')
// Вывод: 1, 2, 3Предсказание порядка вывода: синхронный код + Promise.then + setTimeout + queueMicrotask
// Задача: предсказать порядок вывода ПЕРЕД запуском кода
console.log('Старт') // 1 — синхронно
// Макрозадача #1 (попадёт в Task Queue)
setTimeout(() => {
console.log('setTimeout 1') // 5 — первая макрозадача
}, 0)
// Цепочка промисов — всё это микрозадачи
Promise.resolve()
.then(() => {
console.log('Promise 1') // 3 — первая микрозадача
// Добавляет ещё макрозадачу
setTimeout(() => {
console.log('setTimeout 2') // 6 — вторая макрозадача
}, 0)
})
.then(() => {
console.log('Promise 2') // 4 — вторая микрозадача (добавлена после Promise 1)
})
// Ещё одна микрозадача напрямую
queueMicrotask(() => {
console.log('queueMicrotask') // тоже микрозадача
})
console.log('Конец') // 2 — синхронно
// Что в очереди после синхронного кода:
// Microtask Queue: [Promise 1 callback, queueMicrotask callback]
// (Promise.resolve().then добавлен раньше, queueMicrotask — позже)
// Task Queue: [setTimeout 1 callback]
// Порядок выполнения:
// 1. Синхронно: 'Старт', 'Конец'
// 2. Микрозадачи: 'Promise 1' → внутри добавляется 'Promise 2' в очередь
// 3. Ещё микрозадача: 'queueMicrotask'
// 4. Ещё микрозадача: 'Promise 2' (добавлена внутри Promise 1)
// 5. Макрозадача: 'setTimeout 1'
// 6. Макрозадача: 'setTimeout 2'
// Итоговый вывод:
// Старт
// Конец
// Promise 1
// queueMicrotask
// Promise 2
// setTimeout 1
// setTimeout 2На собеседовании тебе показывают 10 строк с setTimeout, Promise.then и console.log и спрашивают: в каком порядке выведет консоль? Без понимания Event Loop — загадка. С пониманием — задача решается по алгоритму за 30 секунд.
.then(), .catch(), .finally() — источники микрозадачawait под капотом создаёт микрозадачуКогда вы вызываете функцию, она добавляется на вершину стека. Когда завершается — удаляется:
function greet(name) {
return `Привет, ${name}!`
}
function main() {
const msg = greet('Иван') // greet попадает в стек
console.log(msg) // console.log попадает в стек
}
main()
// Стек: [main] → [main, greet] → [main] → [main, console.log] → []Макрозадачи попадают в Task Queue (очередь задач):
setTimeout(fn, delay)setInterval(fn, delay)setTimeout(() => console.log('Макрозадача'), 0)
// Даже с задержкой 0 — выполнится ПОСЛЕ всего синхронного кодаМикрозадачи попадают в Microtask Queue:
Promise.then, Promise.catch, Promise.finallyqueueMicrotask(fn)MutationObserverPromise.resolve().then(() => console.log('Микрозадача'))
// Выполнится после синхронного кода, но ПЕРЕД setTimeout1. Выполнить весь синхронный код (пока стек не опустеет)
2. Выполнить все микрозадачи из Microtask Queue (до опустошения)
3. Выполнить одну макрозадачу из Task Queue
4. Снова выполнить все микрозадачи
5. Повторить с шага 3
console.log('1 — синхронный')
setTimeout(() => console.log('4 — макрозадача'), 0)
Promise.resolve()
.then(() => console.log('2 — микрозадача'))
.then(() => console.log('3 — вторая микрозадача'))
console.log('После Promise.resolve — синхронный')
// Вывод:
// 1 — синхронный
// После Promise.resolve — синхронный
// 2 — микрозадача
// 3 — вторая микрозадача
// 4 — макрозадачаХотя setTimeout(fn, 0) означает "выполни как можно скорее", он всё равно помещается в Task Queue (макрозадачи). После выполнения синхронного кода Event Loop сначала опустошает очередь микрозадач, и только потом берёт одну макрозадачу.
queueMicrotask(fn) позволяет добавить функцию в очередь микрозадач напрямую, без создания Promise:
console.log('начало')
queueMicrotask(() => console.log('микрозадача'))
console.log('конец')
// начало → конец → микрозадачаconsole.log('A')
setTimeout(() => console.log('B'), 0)
Promise.resolve()
.then(() => {
console.log('C')
setTimeout(() => console.log('D'), 0)
})
.then(() => console.log('E'))
queueMicrotask(() => console.log('F'))
console.log('G')
// Порядок: A, G, C, F, E, B, D
// A, G — синхронный код
// C — первая .then() (микрозадача)
// F — queueMicrotask (добавлена до .then, но обе в очереди)
// E — вторая .then() (добавлена внутри C)
// B — первый setTimeout (макрозадача)
// D — второй setTimeout (добавлен внутри C, следующая макрозадача)1. Ожидание, что setTimeout(fn, 0) выполнится "немедленно":
let data = null
setTimeout(() => { data = 'загружено' }, 0)
console.log(data) // null — setTimeout ещё не выполнился!
// Правило: код после setTimeout выполняется ДО колбэка,
// даже с задержкой 02. Бесконечный цикл микрозадач блокирует рендеринг:
// Плохо: рекурсивные Promise бесконечно добавляют микрозадачи,
// браузер не может отрисовать следующий кадр
function badLoop() {
Promise.resolve().then(badLoop) // бесконечно добавляет микрозадачи
}
badLoop()
// Хорошо: используй requestAnimationFrame или setTimeout для периодических задач
function goodLoop() {
setTimeout(goodLoop, 0) // макрозадача — браузер может отрисовать кадр между итерациями
}3. async/await создаёт микрозадачи в неочевидных местах:
async function main() {
console.log('1')
await Promise.resolve() // await = .then = микрозадача
console.log('3') // выполнится после синхронного кода ВЫЗЫВАЮЩЕГО
}
main()
console.log('2') // выполнится ДО console.log('3')
// Вывод: 1, 2, 3Предсказание порядка вывода: синхронный код + Promise.then + setTimeout + queueMicrotask
// Задача: предсказать порядок вывода ПЕРЕД запуском кода
console.log('Старт') // 1 — синхронно
// Макрозадача #1 (попадёт в Task Queue)
setTimeout(() => {
console.log('setTimeout 1') // 5 — первая макрозадача
}, 0)
// Цепочка промисов — всё это микрозадачи
Promise.resolve()
.then(() => {
console.log('Promise 1') // 3 — первая микрозадача
// Добавляет ещё макрозадачу
setTimeout(() => {
console.log('setTimeout 2') // 6 — вторая макрозадача
}, 0)
})
.then(() => {
console.log('Promise 2') // 4 — вторая микрозадача (добавлена после Promise 1)
})
// Ещё одна микрозадача напрямую
queueMicrotask(() => {
console.log('queueMicrotask') // тоже микрозадача
})
console.log('Конец') // 2 — синхронно
// Что в очереди после синхронного кода:
// Microtask Queue: [Promise 1 callback, queueMicrotask callback]
// (Promise.resolve().then добавлен раньше, queueMicrotask — позже)
// Task Queue: [setTimeout 1 callback]
// Порядок выполнения:
// 1. Синхронно: 'Старт', 'Конец'
// 2. Микрозадачи: 'Promise 1' → внутри добавляется 'Promise 2' в очередь
// 3. Ещё микрозадача: 'queueMicrotask'
// 4. Ещё микрозадача: 'Promise 2' (добавлена внутри Promise 1)
// 5. Макрозадача: 'setTimeout 1'
// 6. Макрозадача: 'setTimeout 2'
// Итоговый вывод:
// Старт
// Конец
// Promise 1
// queueMicrotask
// Promise 2
// setTimeout 1
// setTimeout 2Расставь код так, чтобы числа выводились строго в порядке: 1, 2, 3, 4. Используй синхронный console.log для 1, queueMicrotask для 2, вложенный queueMicrotask для 3, и setTimeout(fn, 0) для 4.
Синхронный код выполняется первым — console.log(1) просто пишем без обёртки. queueMicrotask(() => { console.log(2); queueMicrotask(() => console.log(3)) }) — вложенный queueMicrotask добавляется в очередь уже во время выполнения микрозадач, поэтому выполняется следующим. setTimeout(() => console.log(4), 0) — макрозадача, выполняется последней.