1. Введение в Node.js и основы JavaScript: Event Loop и асинхронность
Введение в Node.js и основы JavaScript: Event Loop и асинхронность
Представьте себе ресторан, в котором работает всего один официант. Если он примет заказ у первого столика и будет стоять на кухне, ожидая, пока повар приготовит блюдо, остальные посетители просто уйдут. Именно так работают традиционные многопоточные серверы, выделяя отдельный поток (официанта) на каждый запрос. Но что, если официант передаст заказ на кухню и сразу пойдет обслуживать следующий столик, а когда блюдо будет готово, просто заберет его? Это и есть фундаментальный принцип работы Node.js.
Среда выполнения Node.js построена на движке V8 и позволяет выполнять код JavaScript вне браузера. Главная особенность этой среды — однопоточность в сочетании с неблокирующим вводом-выводом.
Синхронность и асинхронность: математика времени выполнения
В классическом синхронном программировании каждая следующая строчка кода ждет завершения предыдущей. Если программа обращается к базе данных, весь процесс замирает до получения ответа.
Рассмотрим затраты времени на выполнение двух независимых операций: запрос к базе данных занимает 500 миллисекунд, а чтение файла с диска — 300 миллисекунд.
В синхронной модели общее время выполнения вычисляется путем сложения:
Где — общее время синхронного выполнения, — время первой операции (500 мс), — время второй операции (300 мс). В итоге сервер потратит 800 миллисекунд.
В асинхронной модели операции запускаются практически одновременно, и программа не ждет их завершения. Общее время будет равно времени самой долгой операции:
Где — общее время асинхронного выполнения, а функция выбирает наибольшее значение из и . В нашем случае сервер справится за 500 миллисекунд, сэкономив 37,5% времени.
Пропускная способность сервера напрямую зависит от времени обработки запроса. Ее можно выразить формулой:
Где — количество запросов в секунду, — общее количество запросов, — время в секундах. Если сервер обрабатывает 1000 запросов синхронно по 0,8 секунды каждый, общее время составит 800 секунд. Пропускная способность будет равна 1,25 запроса в секунду. В асинхронной модели, где 1000 запросов инициируются почти мгновенно и каждый ждет ответа 0,5 секунды, общее время составит около 1,5 секунд (с учетом накладных расходов). Пропускная способность взлетит до 666 запросов в секунду.
| Характеристика | Синхронный подход | Асинхронный подход (Node.js) | | --- | --- | --- | | Потребление памяти | Высокое (нужен новый поток на каждый запрос) | Низкое (один поток обрабатывает всё) | | Скорость при I/O операциях | Низкая (поток простаивает в ожидании) | Высокая (поток переключается на другие задачи) | | Сложность кода | Простая, линейная логика | Требует понимания коллбэков и промисов |
Анатомия цикла событий
Сердцем асинхронности в Node.js является Event Loop (цикл событий). Это механизм, который позволяет однопоточному JavaScript выполнять неблокирующие операции, делегируя тяжелую работу операционной системе.
> Event Loop — это то, что позволяет Node.js выполнять асинхронные операции ввода-вывода, несмотря на то, что JavaScript является однопоточным, путем выгрузки операций в ядро системы, когда это возможно. > > Официальная документация Node.js
Чтобы понять, как код не превращается в хаос, нужно разобрать четыре основных компонента архитектуры:
Фазы Event Loop в Node.js: заглядываем под капот
В отличие от браузера, где Event Loop относительно прост, в Node.js он реализован на базе библиотеки libuv и состоит из нескольких строго определенных фаз. Каждая фаза имеет свою собственную очередь коллбэков.
setTimeout и setInterval.setImmediate.socket.on('close', ...).Между каждой из этих фаз Node.js останавливается, чтобы проверить очередь микрозадач. Если там есть задачи, они выполняются до полного опустошения очереди, и только потом Event Loop переходит к следующей фазе.
Рассмотрим пример с числами, чтобы понять разницу между setTimeout и setImmediate. Допустим, мы запускаем обе функции в пустом файле:
Порядок вывода в данном случае непредсказуем. Задержка в 0 миллисекунд на самом деле округляется до 1 миллисекунды. Если Event Loop запустится быстрее, чем пройдет 1 миллисекунда, фаза Timers окажется пустой, и движок перейдет к фазе Check, выведя Immediate первым.
Однако, если мы поместим этот же код внутрь коллбэка чтения файла (фаза Poll):
Здесь порядок гарантирован. После завершения чтения файла мы находимся на фазе Poll. Следующая фаза по кругу — Check. Поэтому setImmediate выполнится первым, а setTimeout останется ждать следующего витка цикла событий, чтобы выполниться на фазе Timers.
Макрозадачи и микрозадачи: иерархия приоритетов
Разделение на макро- и микрозадачи — ключевой момент для понимания асинхронности.
К макрозадачам относятся:
setTimeout и setIntervalК микрозадачам относятся:
.then, .catch, .finally)queueMicrotaskprocess.nextTick (специфично для Node.js, имеет наивысший приоритет даже среди микрозадач)Рассмотрим классический пример:
Проследим за выполнением этого кода с точки зрения движка:
Шаг 1.setTimeout. Он передает таймер в Node APIs. Поскольку задержка равна 0 миллисекунд, коллбэк почти мгновенно попадает в Task Queue.Promise.resolve(). Обработчик .then немедленно помещается в Microtask Queue.Шаг 4.Шаг 3.Шаг 2.Несмотря на то, что таймер был установлен с нулевой задержкой и объявлен раньше промиса, микрозадача всегда имеет приоритет над макрозадачей. Если микрозадачи будут бесконечно добавлять новые микрозадачи в очередь, Event Loop никогда не доберется до макрозадач, что приведет к зависанию программы.