← JavaScript/Событийный цикл: микрозадачи и макрозадачи#120 из 383← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

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

На собеседовании тебе показывают 10 строк с setTimeout, Promise.then и console.log и спрашивают: в каком порядке выведет консоль? Без понимания Event Loop — загадка. С пониманием — задача решается по алгоритму за 30 секунд.

На основе предыдущих уроков

  • «setTimeout» — макрозадачи: функции в очереди задач
  • «Промисы» — .then(), .catch(), .finally() — источники микрозадач
  • «async/await» — await под капотом создаёт микрозадачу
  • Стек вызовов (Call Stack)

    Когда вы вызываете функцию, она добавляется на вершину стека. Когда завершается — удаляется:

    function greet(name) {
      return `Привет, ${name}!`
    }
    
    function main() {
      const msg = greet('Иван')  // greet попадает в стек
      console.log(msg)           // console.log попадает в стек
    }
    
    main()
    // Стек: [main] → [main, greet] → [main] → [main, console.log] → []

    Макрозадачи (Macrotasks)

    Макрозадачи попадают в Task Queue (очередь задач):

  • setTimeout(fn, delay)
  • setInterval(fn, delay)
  • События браузера (click, keypress, load)
  • I/O операции (чтение файлов в Node.js)
  • setTimeout(() => console.log('Макрозадача'), 0)
    // Даже с задержкой 0 — выполнится ПОСЛЕ всего синхронного кода

    Микрозадачи (Microtasks)

    Микрозадачи попадают в Microtask Queue:

  • Promise.then, Promise.catch, Promise.finally
  • queueMicrotask(fn)
  • MutationObserver
  • Promise.resolve().then(() => console.log('Микрозадача'))
    // Выполнится после синхронного кода, но ПЕРЕД setTimeout

    Порядок выполнения Event Loop

    1. Выполнить весь синхронный код (пока стек не опустеет)

    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 — макрозадача

    Почему Promise.then выполняется раньше setTimeout(fn, 0)?

    Хотя setTimeout(fn, 0) означает "выполни как можно скорее", он всё равно помещается в Task Queue (макрозадачи). После выполнения синхронного кода Event Loop сначала опустошает очередь микрозадач, и только потом берёт одну макрозадачу.

    queueMicrotask

    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 выполняется ДО колбэка,
    // даже с задержкой 0

    2. Бесконечный цикл микрозадач блокирует рендеринг:

    // Плохо: рекурсивные 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

    В реальных проектах

  • Собеседования — вопрос "в каком порядке выведется?" — классика технических интервью
  • UI-оптимизация — понимание что тяжёлая синхронная операция блокирует рендеринг
  • Дебаггинг race conditions — почему state обновился раньше чем ожидалось
  • Node.js — порядок выполнения I/O callback vs process.nextTick vs setImmediate
  • Примеры

    Предсказание порядка вывода: синхронный код + 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 секунд.

    На основе предыдущих уроков

  • «setTimeout» — макрозадачи: функции в очереди задач
  • «Промисы» — .then(), .catch(), .finally() — источники микрозадач
  • «async/await» — await под капотом создаёт микрозадачу
  • Стек вызовов (Call Stack)

    Когда вы вызываете функцию, она добавляется на вершину стека. Когда завершается — удаляется:

    function greet(name) {
      return `Привет, ${name}!`
    }
    
    function main() {
      const msg = greet('Иван')  // greet попадает в стек
      console.log(msg)           // console.log попадает в стек
    }
    
    main()
    // Стек: [main] → [main, greet] → [main] → [main, console.log] → []

    Макрозадачи (Macrotasks)

    Макрозадачи попадают в Task Queue (очередь задач):

  • setTimeout(fn, delay)
  • setInterval(fn, delay)
  • События браузера (click, keypress, load)
  • I/O операции (чтение файлов в Node.js)
  • setTimeout(() => console.log('Макрозадача'), 0)
    // Даже с задержкой 0 — выполнится ПОСЛЕ всего синхронного кода

    Микрозадачи (Microtasks)

    Микрозадачи попадают в Microtask Queue:

  • Promise.then, Promise.catch, Promise.finally
  • queueMicrotask(fn)
  • MutationObserver
  • Promise.resolve().then(() => console.log('Микрозадача'))
    // Выполнится после синхронного кода, но ПЕРЕД setTimeout

    Порядок выполнения Event Loop

    1. Выполнить весь синхронный код (пока стек не опустеет)

    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 — макрозадача

    Почему Promise.then выполняется раньше setTimeout(fn, 0)?

    Хотя setTimeout(fn, 0) означает "выполни как можно скорее", он всё равно помещается в Task Queue (макрозадачи). После выполнения синхронного кода Event Loop сначала опустошает очередь микрозадач, и только потом берёт одну макрозадачу.

    queueMicrotask

    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 выполняется ДО колбэка,
    // даже с задержкой 0

    2. Бесконечный цикл микрозадач блокирует рендеринг:

    // Плохо: рекурсивные 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

    В реальных проектах

  • Собеседования — вопрос "в каком порядке выведется?" — классика технических интервью
  • UI-оптимизация — понимание что тяжёлая синхронная операция блокирует рендеринг
  • Дебаггинг race conditions — почему state обновился раньше чем ожидалось
  • Node.js — порядок выполнения I/O callback vs process.nextTick vs setImmediate
  • Примеры

    Предсказание порядка вывода: синхронный код + 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) — макрозадача, выполняется последней.

    Загружаем среду выполнения...
    Загружаем AI-помощника...