На собеседовании тебе показывают 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('Микрозадача'))
// Выполнится после синхронного кода, но ПЕРЕД setTimeoutconsole.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('Микрозадача'))
// Выполнится после синхронного кода, но ПЕРЕД setTimeoutconsole.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) — макрозадача, выполняется последней.