Event Loop в JavaScript: микрозадачи и макрозадачи с примерами

Event Loop в JavaScript: микрозадачи и макрозадачи с примерами

Event Loop в JavaScript: микрозадачи и макрозадачи с примерами

Event Loop в JavaScript — базовый механизм, который определяет, когда выполняется ваш код: синхронный, коллбэки, промисы, таймеры и обработчики событий. Понимание очередей микрозадач и макрозадач помогает писать плавные интерфейсы, избегать лагов и уверенно проходить собеседования.

Event Loop JavaScript: микрозадачи и макрозадачи — что это

JavaScript однопоточен: в каждый момент времени исполняется одна операция. Остальные «ждут» своей очереди в специальных очередях задач, которыми управляет Event Loop.

  • Микрозадачи (microtasks): коллбэки Promise.then/catch/finally, queueMicrotask, MutationObserver. Выполняются сразу после текущего стека синхронного кода и до следующей макрозадачи и перерисовки.
  • Макрозадачи (macrotasks): setTimeout, setInterval, обработчики DOM-событий, MessageChannel, сетевые коллбэки и т. п. Выполняются после завершения микрозадач и возможной перерисовки.
  • Общий цикл: синхронный код → все микрозадачи → возможно рендер → одна макрозадача → снова микрозадачи → рендер → …

    Порядок выполнения: наглядный пример

    console.log('A');
    
    setTimeout(() => console.log('B setTimeout'), 0);
    
    Promise.resolve().then(() => console.log('C microtask: Promise.then'));
    
    queueMicrotask(() => console.log('D microtask: queueMicrotask'));
    
    console.log('E');
    

    Вывод будет: A, E, C, D, B. Сначала — синхронные A, E, потом микрозадачи (C, затем D в порядке постановки), и лишь затем макрозадача от setTimeout.

    Async/await и микрозадачи

    await ставит продолжение функции в очередь микрозадач (через Promise).

    async function demo() {
      console.log('1');
      await null; // то же, что await Promise.resolve(null)
      console.log('2');
    }
    
    console.log('0');
    
    demo();
    
    console.log('3');
    

    Порядок: 0, 1, 3, 2. После await выполнение demo «уходит» в микрозадачу, которая сработает после завершения текущего синхронного стека.

    Практика: как не блокировать интерфейс

    Длинные циклы и тяжелые вычисления «замораживают» UI, ведь главный поток занят. Решение — разбивать работу на порции и уступать управление циклу событий, позволяя браузеру отрисовать кадр.

    Плохой пример

    // Блокирует поток, интерфейс подвисает
    for (let i = 0; i < 1e8; i++) {
      // тяжёлая работа
    }
    console.log('Готово');
    

    Разбиение на чанки + уступаем кадр

    // Уступаем отрисовке через requestAnimationFrame
    function nextFrame() {
      return new Promise(resolve => requestAnimationFrame(resolve));
    }
    
    async function processInChunks(items, chunkSize = 1000) {
      for (let i = 0; i < items.length; i += chunkSize) {
        const chunk = items.slice(i, i + chunkSize);
        // обработка порции
        for (const x of chunk) {
          // ... работа с x
        }
        // уступаем управление, чтобы браузер успел отрисовать кадр
        await nextFrame();
      }
      console.log('Готово без фризов');
    }
    

    Почему не microtask для этого? Микрозадачи выполняются до отрисовки. Если вставлять await Promise.resolve() в цикл, вы не дадите браузеру «вдохнуть». Используйте requestAnimationFrame или хотя бы setTimeout(..., 0) для уступки следующей макрозадаче.

    Уступка через setTimeout и MessageChannel

    // Макрозадача с минимальной задержкой
    await new Promise(r => setTimeout(r, 0));
    
    // Или MessageChannel — часто быстрее, чем setTimeout(0)
    const ch = new MessageChannel();
    ch.port1.onmessage = () => console.log('macrotask via MessageChannel');
    ch.port2.postMessage(null);
    

    Частые ошибки и как их исправить

  • Бесконечные микрозадачи «голодают» цикл.

    function spin() {
      Promise.resolve().then(spin); // микрозадача снова ставит микрозадачу
    }
    spin(); // интерфейс перестанет отрисовываться
    

    Исправление — периодически отдавайте управление через макрозадачу:

    function spinSafe() {
      setTimeout(spinSafe, 0); // макрозадача даёт шанс отрисовке и другим задачам
    }
    spinSafe();
    
  • async в forEach не «ждёт» внутри цикла.

    const items = [1,2,3];
    items.forEach(async (x) => {
      await doWork(x);
    });
    console.log('Готово?'); // выведется раньше, чем doWork завершится
    

    Правильно — использовать for...of с await или Promise.all:

    // Последовательно
    for (const x of items) {
      await doWork(x);
    }
    console.log('Готово!');
    
    // Параллельно
    await Promise.all(items.map(doWork));
    console.log('Готово параллельно!');
    
  • Непонимание приоритета: почему Promise быстрее setTimeout?

    Потому что then — микрозадача, которая выполняется до макрозадач. Запомните: «Promises first, timers later».

  • Короткая шпаргалка по Event Loop

  • Сначала весь синхронный код, затем все микрозадачи, потом рендер и лишь затем следующая макрозадача.
  • Микрозадачи: Promise.then/catch/finally, queueMicrotask, MutationObserver.
  • Макрозадачи: setTimeout, setInterval, MessageChannel, DOM-события, сетевые коллбэки.
  • await продолжает функцию как микрозадачу.
  • Чтобы дать браузеру отрисовать интерфейс — используйте requestAnimationFrame или макрозадачу.
  • Не используйте бесконечные цепочки микрозадач — это «голодание» рендера.
  • Мини-викторина для собеседования

    console.log(1);
    setTimeout(() => console.log(2), 0);
    Promise.resolve().then(() => console.log(3));
    Promise.resolve().then(() => setTimeout(() => console.log(4), 0));
    console.log(5);
    

    Ответ: 1, 5, 3, 2, 4. Объяснение: синхронно — 1 и 5, затем микрозадачи — 3, затем макрозадачи в порядке постановки — 2, и потом 4 (таймер, добавленный из микрозадачи).

    Небольшое отличие в Node.js

    В Node.js есть ещё process.nextTick — спец. очередь, выполняется раньше промисов (ещё «более микро»). Для браузера достаточно помнить базовое правило о микро- и макрозадачах, а в Node — аккуратно использовать nextTick, чтобы не блокировать цикл событий.

    Что дальше изучать

    Закрепите тему на практике: поэкспериментируйте с разными комбинациями Promise, setTimeout, MessageChannel, попробуйте «распилить» тяжёлую задачу с уступкой кадра. А если хотите пройти структурированный путь от основ до продвинутых тем (включая асинхронность, работу с DOM и проектные практики), рекомендую практический курс «JavaScript с Нуля до Гуру 2.0» — разобраться и прокачаться на реальных задачах.

    Источник

    НЕТ КОММЕНТАРИЕВ

    Оставить комментарий