Демультиплексор событий
Одновременно запустите таймер, чтение файла и DNS, а затем наблюдайте возврат готовых callbacks.
Временная шкала
Разбираем: Демультиплексор событий
Этот раздел можно читать до запуска опыта. После теории вернитесь к live trace и сопоставьте каждый шаг с реальным событием.
Это похоже на диспетчера в гостинице. Он не стоит у каждой двери и не спрашивает каждую секунду, готов ли номер. Службы сообщают диспетчеру о готовности, а тот передаёт уведомления одному администратору — JavaScript-потоку.
Node регистрирует асинхронную работу в libuv и возвращает управление приложению. Для сокетов libuv использует механизмы готовности ОС, обычные файлы и dns.lookup обычно передаёт нативному thread pool, а таймер хранит как временной порог. Это разные внутренние пути, которые сходятся при возврате callbacks в JavaScript.
Термины этого эксперимента
Сначала поймите слова — затем порядок выполнения.
Event source
Источник готовности: сокет, таймер, файловая операция, DNS или сообщение другого потока.
Demultiplexer
Механизм, который ждёт множество источников и возвращает список тех, которые готовы.
libuv
Нативная библиотека Node, унифицирующая Event Loop и асинхронные операции на разных ОС.
Poll
Фаза, где Node обрабатывает многие готовые I/O callbacks и при необходимости ожидает новые события.
Что происходит по шагам
Каждый шаг соответствует наблюдаемому состоянию runtime.
- 01Регистрация
JS вызывает timer, readFile и DNS, но не ждёт их результат на месте.
- 02Делегирование
libuv использует таймерную структуру, ОС или свой пул потоков.
- 03Свободный стек
Между регистрацией и готовностью Node может выполнять другие запросы.
- 04Сигнал готовности
Demultiplexer/libuv узнаёт, какой источник закончил работу.
- 05Callback
Готовая функция ставится на обработку и входит в JS-стек, когда тот свободен.
Где результат требует оговорки
Эти детали объясняют, почему похожий код иногда даёт другой trace.
Не каждый источник проходит один путь
Сетевой сокет обычно наблюдается через epoll, kqueue или IOCP; обычный файл часто обслуживает thread pool; таймер вообще является проверкой временного порога. «Демультиплексор» здесь — удобная общая модель, а не один универсальный объект для всех API.
dns.lookup и dns.resolve устроены по-разному
dns.lookup использует системный resolver и обычно общий пул libuv. Методы dns.resolve* выполняют DNS-запросы другим путём. Слово DNS само по себе ещё не говорит, какой механизм задействован.
Promise.all ничего не запускает
readFile, lookup и таймер стартуют при вычислении выражений выше. Promise.all получает уже созданные Promises, сохраняет порядок результатов и только координирует ожидание.
Быстрый reject не означает отмену
В успешном сценарии Promise.all ждёт все три результата. Если один Promise отклонится, общий Promise отклонится сразу, но остальные операции не отменятся автоматически.
Сначала разберитесь, какие части Node участвуют в выполнении.
Затем уберите служебные детали и рассмотрите только главную идею.
После этого сопоставьте модель с кодом, который создаёт live trace.
Минимальная модель без служебного кода
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]);Полный код, который выполняет сценарий
Это не альтернативный пример: ниже показаны функции и файлы, используемые кнопкой запуска.
Код сформирован из реальной серверной функции. Для сценариев с отдельным процессом или Worker показаны все участвующие файлы.
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-поток открытым до завершения сценария.
Популярные заблуждения
Миф слева, корректная модель справа.
Node постоянно вызывает каждую функцию и спрашивает, готова ли она.
Ожидание выполняется эффективными механизмами ОС и libuv.
На каждую асинхронную операцию создаётся новый поток.
Сокеты обычно используют готовность ОС; ограниченный thread pool нужен лишь части API.
Если операции завершились параллельно, их JS-callbacks тоже работают параллельно.
В одной JS-среде callbacks по-прежнему входят в главный стек по одному.
Promise.all запускает операции и управляет их отменой.
Операции обычно уже запущены при создании Promises. Для отмены нужен отдельный механизм, например AbortSignal, если конкретный API его поддерживает.
Ответьте своими словами
Если ответ получается объяснить без терминов из документации, ментальная модель уже начала складываться.
- Почему быстрый DNS не обязан ждать медленный таймер?
- Какая часть схемы отличается для сетевого сокета и обычного файла?