NNODE LOOP LABruntime observatoryNNEON · Статьи на 90 языках
CONNECTING
v24.18.0linux/x64
Стек → очереди → фазы
01

Порядок Event Loop

Сравните sync-код, nextTick, microtasks, timers, poll и check в одном живом запуске.

PROCESS IDтекущий сервер
UPTIMEпосле запуска
LOOP DELAY P95perf_hooks
UTILIZATIONevent loop
HTTP ROUNDTRIPbrowser → server
LIVE TRACE

Временная шкала

ГОТОВ
0 ms
События появятся здесьЗапустите выбранный сценарий
#ВРЕМЯИСТОЧНИКСОБЫТИЕ
Ожидаю запуск эксперимента…
ГЛАВА 01
ПОДРОБНЫЙ РАЗБОР · ОТ БАЗЫ К КОДУ

Разбираем: Порядок Event Loop

Этот раздел можно читать до запуска опыта. После теории вернитесь к live trace и сопоставьте каждый шаг с реальным событием.

СНАЧАЛА ПРОСТЫМИ СЛОВАМИ

Представьте одного повара и несколько полок с заказами разного приоритета. Записи в блокноте делаются сверху вниз, но это ещё не порядок приготовления: закончив текущее действие, повар выбирает следующую работу по полке и контексту. В обычном callback Node сначала проверяет nextTick, затем microtasks и после этого продолжает фазы Event Loop.

ТЕХНИЧЕСКАЯ ОСНОВА

JavaScript-код одного Node-процесса обычно исполняется в одном главном потоке по правилу run-to-completion: начатая функция не прерывается другим callback. Строки вызова выполняются сверху вниз, но nextTick, then, setTimeout и setImmediate на своих строках лишь регистрируют функции для будущего выполнения. Event Loop выбирает callback после освобождения стека; между разными очередями приоритет важнее визуального порядка строк.

Зачем это знатьПорядок регистрации помогает предсказывать работу внутри одной очереди, а правила очередей и фаз — между разными типами задач. Смешивание этих уровней приводит к гонкам, starvation и тестам, которые проходят только в одном окружении.
ГДЕ ВЫПОЛНЯЕТСЯ РАБОТА
01ВАШ JS-КОДfunctions · callbacks
02NODE APIsfs · crypto · timers
03V8 + LIBUVheap · loop · pool
04ОПЕРАЦИОННАЯ СИСТЕМАI/O · threads · memory
01 · СЛОВАРЬ

Термины этого эксперимента

Сначала поймите слова — затем порядок выполнения.

01

Call Stack

Стек вызываемых сейчас функций. Пока он не пуст, другой callback не начнёт выполнять JavaScript.

02

Callback

Функция, которую runtime вызовет позже после события: таймера, I/O, сообщения Worker и т. п.

03

Microtask

Приоритетное продолжение Promise или queueMicrotask. Очередь очищается между callbacks и фазами.

04

Фаза

Этап оборота libuv Event Loop. Для лаборатории важны timers, poll и check.

05

Регистрация

Синхронный момент, когда runtime получает callback и условия его будущего запуска. Регистрация ещё не является выполнением callback.

02 · МЕХАНИКА

Что происходит по шагам

Каждый шаг соответствует наблюдаемому состоянию runtime.

  1. 01
    Выполняется sync-код

    Строки читаются сверху вниз: console.log печатает сразу, а остальные вызовы регистрируют callbacks.

  2. 02
    Стек освобождается

    Node получает возможность выбрать отложенную работу.

  3. 03
    Очистка nextTick

    В контексте этого HTTP-эксперимента process.nextTick имеет специальную очередь Node и выполняется перед Promise.

  4. 04
    Очистка microtasks

    Выполняются Promise.then и queueMicrotask, включая добавленные ими новые microtasks.

  5. 05
    Продолжение фаз

    Event Loop переходит к готовым timers, I/O в poll и setImmediate в check.

03 · КОНТЕКСТ

Где результат требует оговорки

Эти детали объясняют, почему похожий код иногда даёт другой trace.

01

Порядок строк — это порядок регистрации

Сначала действительно выполняется строка nextTick, затем строка Promise и так далее. Но тела стрелочных функций выполнятся позже. Поэтому порядок строк не равен итоговому порядку console.log.

02

Внутри одной очереди важен FIFO

Два Promise.then, зарегистрированные подряд, обычно выполнятся в порядке регистрации. Но nextTick и Promise находятся в разных очередях, поэтому приоритет очереди может обойти более раннюю строку.

03

Timer против immediate зависит от места

В main-модуле setTimeout(0) и setImmediate могут поменяться местами. Если оба созданы внутри одного I/O callback, setImmediate выполняется раньше. Начиная с Node 20 расположение timers относительно poll также изменилось.

04

У top-level ESM есть исключение

ES-модуль сам вычисляется как microtask, поэтому в standalone ESM-файле Promise может оказаться раньше nextTick. Лаборатория регистрирует их из HTTP callback и показывает обычный порядок nextTick → Promise.

01
Теория

Сначала разберитесь, какие части Node участвуют в выполнении.

02
Упрощённый код

Затем уберите служебные детали и рассмотрите только главную идею.

03
Runtime-код

После этого сопоставьте модель с кодом, который создаёт live trace.

04 · Упрощённый код

Минимальная модель без служебного кода

src/demos.js · учебный фрагментJavaScript
console.log('sync'); // Выполняется сейчас

// Эти строки регистрируют callbacks сверху вниз:
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('Promise'));
setTimeout(() => console.log('timer'), 0);
setImmediate(() => console.log('immediate'));

// Их тела выполнятся позже по правилам очередей и фаз.
05 · Runtime-код

Полный код, который выполняет сценарий

Это не альтернативный пример: ниже показаны функции и файлы, используемые кнопкой запуска.

ФАКТИЧЕСКИЙ SOURCE

Код сформирован из реальной серверной функции. Для сценариев с отдельным процессом или Worker показаны все участвующие файлы.

src/demos.js
сценарий116 строк
import { readFile } from 'node:fs';
import { fileURLToPath } from 'node:url';

const packageJsonPath = fileURLToPath(
  new URL('../package.json', import.meta.url),
);

const sleep = (ms) =>
  new Promise((resolve) => setTimeout(resolve, ms));

async function eventLoopOrder(emit) {
  emit('call-stack', 'sync', 'Синхронный код начал выполняться');

  const outerCallbacks = [];
  const waitForOuter = new Promise((resolve) => {
    let completed = 0;
    const done = () => {
      completed += 1;
      if (completed === 4) resolve();
    };

    process.nextTick(() => {
      outerCallbacks.push('process.nextTick');
      emit('nextTick', 'callback', 'process.nextTick callback');
      done();
    });

    Promise.resolve().then(() => {
      outerCallbacks.push('Promise.then');
      emit('microtasks', 'callback', 'Promise.then microtask');
      done();
    });

    queueMicrotask(() => {
      outerCallbacks.push('queueMicrotask');
      emit('microtasks', 'callback', 'queueMicrotask callback');
      done();
    });

    setTimeout(() => {
      outerCallbacks.push('setTimeout(0)');
      emit('timers', 'callback', 'setTimeout(0) callback');
      done();
    }, 0);

    setImmediate(() => {
      outerCallbacks.push('setImmediate');
      emit('check', 'callback', 'setImmediate callback');
      done();
    });
  });

  emit(
    'call-stack',
    'schedule',
    'Callbacks зарегистрированы; синхронный стек сейчас освободится',
  );
  await waitForOuter;

  // Ждём обе макрозадачи. Эксперимент запускается из HTTP callback, поэтому
  // здесь важен runtime-контекст, а не только порядок строк в сниппете.
  while (
    !outerCallbacks.includes('setTimeout(0)') ||
    !outerCallbacks.includes('setImmediate')
  ) {
    await sleep(1);
  }

  emit('result', 'result', `Первый раунд: ${outerCallbacks.join(' → ')}`);

  emit('poll', 'schedule', 'Запускаем fs.readFile и переходим к I/O-раунду');
  await new Promise((resolve, reject) => {
    readFile(packageJsonPath, 'utf8', (error) => {
      if (error) {
        reject(error);
        return;
      }

      emit('poll', 'callback', 'Callback fs.readFile: сейчас мы внутри poll-фазы');
      const ioOrder = [];
      let completed = 0;

      const done = () => {
        completed += 1;
        if (completed === 4) {
          emit('result', 'result', `Внутри I/O: ${ioOrder.join(' → ')}`);
          resolve();
        }
      };

      process.nextTick(() => {
        ioOrder.push('nextTick');
        emit('nextTick', 'callback', 'nextTick, созданный внутри I/O');
        done();
      });

      Promise.resolve().then(() => {
        ioOrder.push('Promise');
        emit('microtasks', 'callback', 'Promise, созданный внутри I/O');
        done();
      });

      setImmediate(() => {
        ioOrder.push('setImmediate');
        emit('check', 'callback', 'setImmediate, созданный внутри I/O');
        done();
      });

      setTimeout(() => {
        ioOrder.push('setTimeout');
        emit('timers', 'callback', 'setTimeout(0), созданный внутри I/O');
        done();
      }, 0);
    });
  });
}

Именно вызовы emit(...) превращаются в строки live trace. await и Promise удерживают HTTP-поток открытым до завершения сценария.

06 · НЕ ПЕРЕПУТАЙТЕ

Популярные заблуждения

Миф слева, корректная модель справа.

МИФ

В Node существует одна общая очередь событий.

НА САМОМ ДЕЛЕ

Очередей и фаз несколько, между ними есть правила приоритета.

МИФ

setTimeout(fn, 0) выполняет fn немедленно.

НА САМОМ ДЕЛЕ

Ноль означает минимальную задержку; callback ещё должен дождаться подходящей фазы и свободного стека.

МИФ

Асинхронная функция может прервать текущий JavaScript.

НА САМОМ ДЕЛЕ

Нет: callback начнётся только после завершения текущего участка кода.

МИФ

Если строка записана выше, её callback обязательно сработает раньше.

НА САМОМ ДЕЛЕ

Это верно лишь при совместимых условиях, например внутри одной FIFO-очереди. Между очередями сначала применяются их приоритеты и правила фаз.

МИФ

process.nextTick всегда и везде раньше Promise.

НА САМОМ ДЕЛЕ

Так происходит в обычных callbacks и CommonJS. При вычислении top-level ESM Promise/microtask может получить преимущество.

07 · САМОПРОВЕРКА

Ответьте своими словами

Если ответ получается объяснить без терминов из документации, ментальная модель уже начала складываться.

  1. Почему Promise не выполняется в момент вызова Promise.resolve()?
  2. Что произойдёт с таймером, если текущая функция работает пять секунд?
  3. Когда порядок строк снова становится порядком callbacks, а когда его переопределяет очередь?