NNODE LOOP LABruntime observatoryNNEON · Статьи на 90 языках
CONNECTING
v24.18.0linux/x64
Много источников → один поток
02

Демультиплексор событий

Одновременно запустите таймер, чтение файла и DNS, а затем наблюдайте возврат готовых callbacks.

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

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

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

Разбираем: Демультиплексор событий

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

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

Это похоже на диспетчера в гостинице. Он не стоит у каждой двери и не спрашивает каждую секунду, готов ли номер. Службы сообщают диспетчеру о готовности, а тот передаёт уведомления одному администратору — JavaScript-потоку.

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

Node регистрирует асинхронную работу в libuv и возвращает управление приложению. Для сокетов libuv использует механизмы готовности ОС, обычные файлы и dns.lookup обычно передаёт нативному thread pool, а таймер хранит как временной порог. Это разные внутренние пути, которые сходятся при возврате callbacks в JavaScript.

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

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

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

01

Event source

Источник готовности: сокет, таймер, файловая операция, DNS или сообщение другого потока.

02

Demultiplexer

Механизм, который ждёт множество источников и возвращает список тех, которые готовы.

03

libuv

Нативная библиотека Node, унифицирующая Event Loop и асинхронные операции на разных ОС.

04

Poll

Фаза, где Node обрабатывает многие готовые I/O callbacks и при необходимости ожидает новые события.

02 · МЕХАНИКА

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

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

  1. 01
    Регистрация

    JS вызывает timer, readFile и DNS, но не ждёт их результат на месте.

  2. 02
    Делегирование

    libuv использует таймерную структуру, ОС или свой пул потоков.

  3. 03
    Свободный стек

    Между регистрацией и готовностью Node может выполнять другие запросы.

  4. 04
    Сигнал готовности

    Demultiplexer/libuv узнаёт, какой источник закончил работу.

  5. 05
    Callback

    Готовая функция ставится на обработку и входит в JS-стек, когда тот свободен.

03 · КОНТЕКСТ

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

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

01

Не каждый источник проходит один путь

Сетевой сокет обычно наблюдается через epoll, kqueue или IOCP; обычный файл часто обслуживает thread pool; таймер вообще является проверкой временного порога. «Демультиплексор» здесь — удобная общая модель, а не один универсальный объект для всех API.

02

dns.lookup и dns.resolve устроены по-разному

dns.lookup использует системный resolver и обычно общий пул libuv. Методы dns.resolve* выполняют DNS-запросы другим путём. Слово DNS само по себе ещё не говорит, какой механизм задействован.

03

Promise.all ничего не запускает

readFile, lookup и таймер стартуют при вычислении выражений выше. Promise.all получает уже созданные Promises, сохраняет порядок результатов и только координирует ожидание.

04

Быстрый reject не означает отмену

В успешном сценарии Promise.all ждёт все три результата. Если один Promise отклонится, общий Promise отклонится сразу, но остальные операции не отменятся автоматически.

01
Теория

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

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

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

03
Runtime-код

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

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

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

src/demos.js · учебный фрагментJavaScript
const file = fs.promises.readFile('package.json');
const dns = dns.promises.lookup('localhost');
const timer = new Promise(r => setTimeout(r, 180));

await Promise.all([file, dns, timer]);
05 · Runtime-код

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

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

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

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

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

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

async function eventDemultiplexer(emit) {
  emit('call-stack', 'sync', 'Регистрируем три независимые операции');

  const timerTask = new Promise((resolve) => {
    emit('timers', 'schedule', 'Таймер 180 мс передан подсистеме таймеров');
    setTimeout(() => {
      emit('timers', 'callback', 'Таймер готов: callback вернулся в JavaScript');
      resolve('timer');
    }, 180);
  });

  const fileTask = new Promise((resolve, reject) => {
    emit('libuv', 'schedule', 'Чтение package.json делегировано libuv');
    readFile(packageJsonPath, 'utf8', (error, content) => {
      if (error) {
        reject(error);
        return;
      }
      emit(
        'poll',
        'callback',
        `Файл готов: получено ${Buffer.byteLength(content)} байт`,
      );
      resolve('file');
    });
  });

  const dnsTask = new Promise((resolve, reject) => {
    emit('libuv', 'schedule', 'DNS lookup localhost делегирован libuv');
    lookup('localhost', (error, address) => {
      if (error) {
        reject(error);
        return;
      }
      emit('poll', 'callback', `DNS готов: localhost → ${address}`);
      resolve('dns');
    });
  });

  emit(
    'demultiplexer',
    'info',
    'JS-стек свободен; Event Loop ожидает сигналы готовности, а не опрашивает каждую функцию вручную',
  );

  const completed = await Promise.all([timerTask, fileTask, dnsTask]);
  emit('result', 'result', `Все источники готовы: ${completed.join(', ')}`);
}

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

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

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

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

МИФ

Node постоянно вызывает каждую функцию и спрашивает, готова ли она.

НА САМОМ ДЕЛЕ

Ожидание выполняется эффективными механизмами ОС и libuv.

МИФ

На каждую асинхронную операцию создаётся новый поток.

НА САМОМ ДЕЛЕ

Сокеты обычно используют готовность ОС; ограниченный thread pool нужен лишь части API.

МИФ

Если операции завершились параллельно, их JS-callbacks тоже работают параллельно.

НА САМОМ ДЕЛЕ

В одной JS-среде callbacks по-прежнему входят в главный стек по одному.

МИФ

Promise.all запускает операции и управляет их отменой.

НА САМОМ ДЕЛЕ

Операции обычно уже запущены при создании Promises. Для отмены нужен отдельный механизм, например AbortSignal, если конкретный API его поддерживает.

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

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

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

  1. Почему быстрый DNS не обязан ждать медленный таймер?
  2. Какая часть схемы отличается для сетевого сокета и обычного файла?