Основы работы с NodeJS

Что такое Node.js

NodeJS, Node.js или просто Node (далее по тексту используется именно Node) это серверная платформа построенная на основе JavaScript движка браузера Google Chrome (V8 Engine). В документации к Node дается следующее краткое определение:

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js' package ecosystem, npm, is the largest ecosystem of open source libraries in the world.

Что переводится как:

Node.js® является исполняемой средой JavaScript, основанной на JavaScript движке Chrome V8 engine. Node.js использует событийную, не блокируемую модель ввода-вывода (I/O), что делает его легковесным и эффективным. Экосистема пакетов Node.js npm это самая большая экосистема библиотек с открытым исходным кодом в мире.

Как видно Node является кросс-платформенной исполняемой средой с открытым исходным кодом, которая предназначена для создания серверных, сетевых или десктопных приложений. Как можно догадаться из приставки JS к названию среды, приложения для Node создаются на JavaScript. Node приложение может запуститься в любой операционной системе при помощи исполняемой среды Node и, при проведении некоторых манипуляций, возможно написание мобильных приложений. Кроме того, Node через npm обладает огромной базой сторонних пакетов, которые могут добавить в ваше приложение практически любую функциональность.

Стоит обратить внимание, что Node является всего-лишь исполняемой средой JavaScript, поддерживающей ввод-вывод. Node не является фреймворком или языком программирования. На нижнем уровне Node может быть описан, как инструмент для создания сетевых программ, использующих протоколы HTTP, TCP, UDP, DNS и SSL, а также программ для чтения и записи данных в файловой системе или памяти компьютера.

Ввод-вывод в наше приложение мы можем реализовать, например, из следующих источников:

  • Базы данных (MySQL, PostgreSQL, MongoDB, Redis, CouchDB)
  • API (программный интерфейс приложения - т.е. интерфейс, предоставляющий программный доступ для сторонних программ) различных приложений (Twitter, Facebook, Apple Push Notifications, VK API)
  • HTTP/Websocket соединения (между браузером и сервером)
  • Файлы
  • Консоль

Особенности Node

  • Асинхронность и управление событиями. Практически все интерфейсы доступа к различным ресурсам (файловая система, сетевые запросы, базы данных и т.д.) в Node являются асинхронными, или, по другому, не блокирующими. Это значит, что при обращении к каким-либо ресурсам Node не ждет ответа от этого ресурса и продолжает выполнение кода дальше. Как только ресурс ответит, механизм обработки событий в Node перехватит этот ответ и его можно будет обработать в специальной функции-колбэке, которую вы написали заранее.
  • Высокая скорость работы. Node основана на Google Chrome V8 JavaScript Engine, это значит, что Node очень быстро интерпретирует JavaScript код.
  • Однопоточное выполнение, но высокая масштабируемость. Node использует однопоточную модель исполнения совместно с циклом событий. Механизм событий позволяет серверу обслуживать подключения без блокировки исполнения кода. Т.е. когда вы обращаетесь к веб-приложению на сервере, созданному по традиционной блокирующейся технологии, обращение к скрипту приложения от других пользователей блокируется до тех пор пока не будет обработан ваш запрос (который включает в себя роутинг, обращение к БД, генерацию ответа и т.д. - т.е. довольно продолжительный по времени), и, соответственно, запросы от других пользователей ждут своей очереди исполнения. Поэтому для таких серверов реализуется поддержка многопроцессности и мультипоточности. Но количество процессов и потоков серьезно ограничиваются возможностями железа сервера. Node напротив использует, как правило, однопоточную модель сервера, но благодаря асинхронной обработке запросов и циклу событий может обслуживать намного большее количество запросов по сравнению с традиционными веб-серверами наподобие Apache. Поэтому про Node говорят, что он использует неблокирующую модель ввода-вывода.
  • Нет буферизации. Node приложения никогда не буферизуют данные. Эти приложения просто выдают данные по кусочкам, что позволяет существенно экономить ресурсы компьютера.
  • Лицензия. Node выпускается под MIT лицензией. Можете делать с Node всё, что душе угодно.

Где можно использовать Node?

Node можно использовать для написания практически любого класса приложений. Например:

  • Приложения ввода-вывода (консольные или оконные)
  • Приложения обработки потока данных
  • Реал-тайм приложения
  • Приложения с API на JSON
  • Одностраничные веб-приложения
  • И т.д.

Кратко о Node

Node, как асинхронная событийная исполняемая среда, спроектирован так, чтобы использовать его при разработке масштабируемых сетевых приложений. В следующем примере с "Hello world" большое количество соединений могут обрабатываться одновременно. Каждое подключение запускает колбэк с кодом для обработки нового соединения, но если соединений нет, то Node приложение как бы "спит".


const http = require('http');

const hostname = 'localhost';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Как было сказано выше, выполнение запросов пользователя не приводит к блокировке исполнения. Запросы пользователя ставятся в очередь событий и выполняются там по мере освобождения ресурсов компьютера. Почти никакая из функций Node не производит ввод-вывод непосредственно сама, поэтому процесс Node никогда не блокируется.

Node представляет новую концепцию под названием "Цикл событий". В некоторых других системах также реализован цикл событий. Например, в Ruby Event Machine или Python Twisted. Но при запуске этого цикла в данных системах с помощью специальной команды (например EventMachine::run()) происходит блокировка дальнейшего исполнения скрипта, в котором создается данный цикл. Поэтому колбэки объявляются в начале скрипта, а запуск цикла событий происходит в самом конце.

В Node создание цикла событий происходит автоматически после окончания работы скрипта. При этом блокировка исполнения скрипта не происходит. Как только заканчиваются все обработчики событий (мы можем указать обработчику, чтобы он вызвался один раз или просто удалить все обработчики), цикл событий завершает свою работу. Обработчики событий как раз и вызывают колбэки. Например выше, в примере кода, Node неявно создает обработчик события поступления запроса от пользователя к серверу. Делается это через вызов:


const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

Особенности нового синтаксиса EcmaScript 6 и разъяснение примера кода

В начале скрипта идет подключение модуля для работы с протоколом HTTP:


const http = require('http');

Протокол HTTP является прикладным протоколом передачи гипертекстовой информации HTML. HTTP работает поверх стандартных протоколов TCP/IP. HTTP используется как правило для передачи данных между сервером с веб-сайтом и клиентом в виде браузера пользователя. Для этого используется стандартный набор методов HTTP, таких как GET, POST, DELETE, PATCH. Сервер и браузер, при установлении между ними соединения, занимаются отправкой друг к другу или обработкой таких запросов с использованием одного из этих методов. В общем, суть HTTP заключается в том, что с использованием одного из указанных методов, браузер пользователя, либо запрашивает данные с сервера (метод GET), например, получение новой страницы, либо отправляет данные на сервер (POST, PATCH), например, отправка данных формы.

Модуль http подключается с помощью специальной функции require. Т.к. до появления EcmaScript 6 в JavaScript не было возможности подключать сторонние модули с кодом, кроме как через тег script в html (что несколько странно для самостоятельного языка), то поэтому для Node был придуман синтаксис под названием CommonJS, который как раз позволял подключать к своему скрипту отдельные файлы с модулями кода. Более подробно про модули в JavaScript можно почитать здесь. Модуль http является стандартным системным модулем Node, поэтому мы просто указываем его имя без использования относительного или абсолютного пути в файловой системе. Несмотря на появление специальной директивы import в EcmaScript 6, мы будем использовать в дальнейшем классический require, т.к. он рекомендуется для использования создателями Node и он используется во всех примерах к документации Node.

const является новой конструкцией языка EcmaScript 6 - современной версии JavaScript. Более подробно ознакомиться с новыми возможностями языка советую здесь. const создает константу, у которой областью видимости являются фигурные скобки {}. Точно также как в языке C. Константу вы инициализируете один раз при ее создании. Как правило в JavaScript в нее помещают объект, методами которого мы планируем пользоваться в дальнейшем. Логично, что этот объект мы не можем заменять другим, но для константы с объектом мы имеем возможность добавлять новые поля и методы - этого константы не запрещают.

В константу server мы помещаем объект сервера, который создается вызовом http.createServer(callback). При создании объекта сервера автоматически создается обработчик события обращения пользователя из браузера к серверу. Каждый раз, как к серверу обращается пользователь, происходит вызов функции обратного вызова callback. Т.к. функция callback объявляется заранее, а вызывается потом при наступлении события, то поэтому данный вид функций называется функциями обратного вызова или просто колбэками.

Функция-колбэк задана в новом EcmaScript 6 синтаксисе, под названием "стрелочная функция": (req, res) => { ... }. Эта запись аналогична классической function(req, res) { ... }, с той лишь разницей, что новая запись короче и значение this в стрелочной функции берется снаружи, т.е. выше по замыканию. Для function значение this определялось в момент вызова функции. Для этого интерпретатор просматривал на каком объекте вызывается функция (т.е. какой объект стоит перед точкой), и ссылка на него записывалась в this. Например obj.func(param) - видно, что функция func(param) вызывается на объекте obj, значит в this запишется ссылка на obj. Если функция вызывается без точки (т.е. не на объекте), то в this присваивается ссылка на глобальный объект (window для браузера, global для Node). Такие сложные правила определения this часто приводили к неожиданным значениям this, что для JavaScript называется потерей контекста исполнения. Поэтому и был придуман новый синтаксис функции, который берет this из внешней области видимости. Особенно удобно использовать стрелочные функции в роли колбэков, где потеря контекста является очень частой проблемой. Функция-колбэк вызывается потом, и на каком объекте она будет вызвана нам будет сложно предугадать, поскольку это происходит внутри обработчика событий.

В колбэк передается два параметра (req,res). Оба являются довольно сложными объектами. Первый объект req содержит параметры запроса пользователя к серверу, а второй res является объектом ответа сервера браузеру, и в всё, что мы запишем в него в качестве ответа, будет автоматически отправлено пользователю в его браузер. Т.к. запрос и ответ являются просто потоком байтов, который передается между сервером и браузером, то чтобы описать параметры запроса или ответа, к ним при передаче прибавляются специальные свойства, называемые заголовками запроса. Они описывают тип контента, размер передаваемых данных, статус соединения и т.д. Подробнее о заголовках поговорим позже.

Внутри функции-колбэка мы видим следующий код:


res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');

В первых двух строках мы записываем заголовки ответа, а в третьей строке пишем текст в тело ответа и сразу же закрываем соединение. Статус-код ответа 200 является стандартным http кодом успешного ответа, наряду с такими неуспешными как 403, 404 и др. Заголовок Content-Type необходимо использовать на каждом ответе, т.к. браузер не всегда в состоянии правильно определить тип передаваемого ему контента. Значением Content-Type является один из стандартных MIME-типов, используемых при передаче по сети различных файлов и правильного определения принимающей стороной типа получаемого контента. Вот здесь можно посмотреть полный список существующих MIME-типов.

В конце скрипта мы видим следующий код:


server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

На объекте сервера server мы вызываем метод server.listen, который запускает сервер на указанном порту. В качестве параметра мы передаем номер порта и имя хоста, который будет ждать запросы пользователя. Последним параметром идет колбэк, запускаемый при старте сервера. Внутри колбэка выводится в консоль информация о сервере. В данном примере используется новая конструкция для задания строк в EcmaScript 6.


`Server running at http://${hostname}:${port}/`

Этот формат строк позволяет добавлять переносы в строку (без необходимости добавлять \ в конец каждой строки при разбиении строки на несколько строк, как было ранее в JavaScript) и поддерживает вставку JavaScript кода прямо в тело строки с помощью конструкции ${JS_code}. Почитать подробнее про новый формат строк можно здесь.

Несмотря на то, что Node изначально спроектирован как однопоточное приложение, это не значит, что вы не можете создавать код с параллельными вычислениями. Дочерние процессы могут создаваться с использованием API child_process.fork(). С помощью модуля cluster, который основан на указанном API, вы можете использовать сокеты для включения синхронизации между процессами.

Установка Node

Во-первых вам необходимо скачать и установить Node. Для этого переходим на сайт nodejs.com, скачиваем и затем устанавливаем Node для своей операционной системы. Установка предельно простая и не требует отдельных пояснений.

Для того, чтобы запустить написанное ранее Node приложение, необходимо открыть консоль или командную строку. Там вы должны перейти в папку с файлом приложения и запустить его через Node. Например, сохраним приведенный выше код сервера с "Hello world" в файл server.js. Затем просто запускаем его консольной командой node server.js. Вот и всё! Сервер работает и вы можете посмотреть на результат в браузере по адресу localhost:3000.

Модули ядра Node

Внутри себя Node имеет небольшой набор стандартных модулей, которые называются также "ядро Node". Эти модули предоставляют вам публичный API, который позволит вам написать практически любую программу. Например, для работы с файловой системой используется модуль fs, а для работы с сетью можно использовать net (TCP), http, dgram (UDP). Напоминаю, что подключить функционал модуля к своему приложению можно командой require(module_name), например require('http'). Эта функция возвращает объект, который предоставляет публичные методы данного модуля. Соответственно необходимо не забыть сохранить этот объект в какую-либо переменную, чтобы можно было пользоваться функционалом модуля в дальнейшем. Более подробно работа с модулями будет описана позднее. Полный пример для подключения модуля http выглядит так:


const http = require('http');

Кроме того, в Node есть модули для асинхронной обработки DNS - dns модуль, модуль для получения специфичной для ОС информации - os, модуль для работы с двоичными массивами данных - buffer, некоторые модули для парсинга URL и адресов файлов (url, querystring, path), модуль для работы с потоками данных - stream и другие модули.

Node обрабатывает ввод-вывод с помощью колбэков, событий, потоков и модулей. Знание этих четырех основ поможет вам в дальнейшем разобраться в логике работы практически любого модуля Node.

Что такое блокирующие и неблокирующие вызовы?

Блокирующийся код характеризуется тем, что новый JavaScript код должен ожидать выполнения незавершенных операций, не относящихся к JavaScript (например чтение с диска, ожидание приема файла через сеть). Это происходит потому что цикл событий не способен продолжить выполнение JavaScript пока выполняется блокирующая операция.

Из-за невысокой скорости интерпретации JavaScript, которая довольно сильно загружает процессор, иногда можно не заметить блокировки исполнения кода. Но это происходит не всегда, и, как правило, блокирующие вызовы сильно замедляют работу реального приложения Node. Блокировка исполнения происходит при использовании так называемых синхронных методов, т.к синхронные методы в Node используют блокирующие методы. Синхронный код - это код который выполняется последовательно. Последующая операция возможна только при полном завершении предыдущей. Названия синхронных методов в Node заканчивается на Sync.

Все методы ввода-вывода в Node поддерживают асинхронную работу, т.е. не блокируют исполнение кода JavaScript. В асинхронном коде последующие операции не ждут полного выполнения предыдущих операций. Для того, чтобы сообщить асинхронному методу, что делать после завершения неблокирующего вызова используется механизм функций-колбэков, которые затем вызываются циклом событий.

Таким образом, блокирующие методы исполняются синхронно, а неблокирующие исполняются асинхронно.

Рассмотрим пример синхронного чтения файла:


const fs = require('fs');
const file = fs.readFileSync('/readme.txt'); // Происходит блокировка, пока файл не будет считан

И эквивалентный асинхронный код:


const fs = require('fs');
fs.readFile('/readme.txt', 
  (err, data) => { // Это функция-колбэк в синтаксисе ES6, передаем ее в readFile
    if (err) throw err; // Если будет ошибка, то будет вызвано исключение и выдана ошибка в консоль
  });

Первый пример выглядит проще, но пока не будет полностью считан файл во второй строке с помощью readFileSync дальнейшее выполнение JavaScript будет заблокировано. Кроме того, в синхронном варианте при ошибке чтения файла произойдет падение скрипта - необходимо дополнительно писать перехват ошибки через try {...} catch {...}. В асинхронном коде мы можем обрабатывать передаваемый в колбэк объект ошибки как нам будет угодно и при ошибке чтения файла не произойдет падение скрипта. Если ошибки нет, то объект ошибки равен null.

Давайте немного расширим наш пример:


const fs = require('fs');
const file = fs.readFileSync('/readme.txt'); // Происходит блокировка, пока файл не будет считан
console.log(file);
// moreWork(); заработает после console.log

И похожий, но не эквивалентный асинхронный код:


const fs = require('fs');
fs.readFile('/readme.txt', (err, data) => {
  if (err) throw err; // Если будет ошибка, то будет вызвано исключение и выдана ошибка в консоль
  console.log(data);
});
// moreWork(); сработает до console.log

В синхронном примере выполнение кода идет последовательно сверху вниз, поэтому console.log вызывается раньше дальнейшей работы скрипта, которую олицетворяет moreWork(). В асинхронном примере вызов fs.readFile является неблокирующим, поэтому сначала выполнится moreWork(), а затем, как считается файл '/readme.txt' произойдет вызов console.log. Возможность продолжения выполнения кода в виде moreWork() (здесь, естественно, может быть любой код), не дожидаясь конца чтения файла, является ключевым фактором повышения скорости работы скриптов на Node.

Например, представим, что запрос к серверу занимает 50мс времени. 45мс из этих 50мс это запрос к базе данных (БД) (операция ввода-вывода). Выбирая асинхронную версию операции с БД мы получаем дополнительный выигрыш в 45мс для обработки других запросов от пользователей.

Колбэки

Понимание смысла функций обратного вызова или колбэков является ключевым моментом изучения работы Node. Очень многие функции в Node являются асинхронными и они используют для своей работы колбэки. Колбеки не являются инновацией Node, а появились гораздо раньше как часть языка JavaScript. Например, мы используем колбэки для обработки различных событий от пользователя в браузере:


document.querySelector('button').addEventListener('click',
  (event) => alert('Вы нажали по ' + event.target.nodeName)
);

В примере выше мы получили с помощью querySelector ссылку на объект кнопки и повесили на него обработчик нажатия этой кнопки при помощи addEventListener. Когда пользователь кликнет по кнопке произойдет вызов колбэка, который был передан вторым аргументом в addEventListener.

Как описывалось ранее, колбэки запускаются асинхронно без блокировки исполнения, или, по другому говоря, позже выполнения основного (синхронного) кода. Вместо последовательного выполнения инструкций кода, асинхронные программы выполняют свои функции-колбэки в зависимости от скорости наступления того или иного события (например, окончание чтения файла, получение блока данных через сеть, ввод данных пользователем в консоль). Поэтому порядок объявления функций-колбэков не влияет на последовательность их вызова.

Напомним, что Node разделяет весь код на два типа: синхронный и асинхронный. Синхронный код это обычный код, инструкции которого вызываются последовательно сверху вниз. Например:


let myNumber = 1;
function addOne() { myNumber++ } // Объявляем функцию
addOne(); // запускаем функцию
console.log(myNumber); // Выводит 2

Здесь на второй строке мы объявляем функцию и на третьей мы ее вызываем не ожидая никаких других событий. Поэтому мы ожидаемо получаем в четвертой строке число 2.

Node, как правило, использует асинхронный код. Для примера считаем наш номер из текстового файла:


const fs = require('fs') // подключаем модуль fs
let myNumber = undefined;

function addOne() {
  fs.readFile('number.txt', (err, fileContents) => {
    myNumber = parseInt(fileContents);
    myNumber++;
  })
}

addOne();

console.log(myNumber); // Мы получаем undefined - эта строка выполнится раньше окончания readFile

Так как readFile является неблокирующим асинхронным методом мы получили в консоли ожидаемое undefined. Обычно методы работы с жестким диском или сетью (т.е. все методы ввода-вывода) мы будем делать асинхронными. А методы, которые используют оперативную память и ресурсы процессора мы будем делать синхронными. Связано это с тем, что методы ввода-вывода работают очень медленно. Например, в среднем, жесткий диск обрабатывает запросы примерно в 100 000 раз медленнее, чем оперативная память.

Когда мы запускаем этот скрипт происходит инициализация всех описанных в нем функций, но, естественно, они вызываются не сразу же. Когда мы вызываем функцию addOne, то она вызывает readFile и сразу же переходит к следующей инструкции после readFile. Если выполнять больше нечего, то Node ожидает окончания чтения диска или работы с сетью, либо же полностью завершает работу скрипта и выходит в консоль.

Здесь стоит обратить внимание, что по умолчанию асинхронная функция readFile передает в в свой колбэк в переменную fileContents не строку, а специфичный для Node объект Buffer, который содержит в себе набор двоичных данных полученных из файла. Чтобы получить именно строку при вызове колбэка из readFile необходимо указать кодировку: fs.readFile('number.txt', 'utf8', callback);. Теперь в callback содержимое файла будет передаваться именно в виде строки. Более подробно об объекте Buffer будет рассказано позднее.

Как только чтение файла будет завершено (это может занять от миллисекунд до нескольких минут в зависимости от скорости и загруженности диска) Node запустит функцию-колбэк, которую мы передали вторым аргументом в readFile. В колбэк будет передан объект ошибки (если есть) и содержимое файла. Очевидно, что console.log в примере выше запустился ранее колбэка, поскольку нигде в коде мы не указали, чтобы console.log запустился после окончания чтения файла.

Как уже было сказано ранее, колбэк это обычная функция, которая будет вызвана позднее работы всего скрипта в результате наступления определенного события. Колбэки следует использовать тогда, когда вы не знаете когда завершится асинхронная операция, но знаете, где она должна завершиться - на последней строке колбэка. Последовательность объявления колбэков не важна, важен порядок вызова колбэков и вложенность колбэков друг в друга, чтобы соблюсти правильный порядок вызовов. Т.к. операции асинхронные, то порядок срабатывания колбеков будет каждый раз разный. Чтобы порядок сохранялся, необходимо вкладывать асинхронные операции и вызов колбэков друг в друга.

Модифицируем пример выше, чтобы сделать правильную последовательность вызовов:


const fs = require('fs');
let myNumber = undefined;

function addOne(output) {
  fs.readFile('number.txt', (err, fileContents) => {
    myNumber = parseInt(fileContents);
    myNumber++;
    output();
  })
}

function log() {
  console.log(myNumber);
}

addOne(log);

Теперь в консоль выводится корректное значение. Рассмотрим последовательность вызовов в приведенном коде подробнее:

  1. Проводится синтаксический анализ кода и поиск синтаксических ошибок. Происходит объявление переменной myNumber, константы fs, функции addOne и функции log. На этом этапе никакой код еще не исполняется.
  2. Затем происходит построчное выполнение скрипта. Вначале происходит присваивание значений в myNumber и fs, и затем происходит запуск функции addOne с параметром в виде ссылки на функцию log. Внутри addOne ссылка на log присваивается в переменную output. И наконец запускается асинхронный метод readFile.
  3. На этом работа синхронной части скрипта заканчивается, больше ничего не выполняется и Node ждет окончания работы readFile. Если что-то необходимо обработать в этот момент, то Node готов это сделать без промедления.
  4. Как только readFile заканчивает чтение файла, Node запускает колбэк, который был вторым аргументом readFile. В него передается объект ошибки (или null если ошибки нет), а вторым аргументом в переменную fileContents записывается содержимое файла. Внутри колбэка содержимое файла преобразуется в число, это число увеличивается на единицу и затем запускается функция log через output(). И теперь вывод в консоль происходит в правильный момент.

Из всей этой последовательности можно заменить, что функции могут вести себя также как объект, т.к. ссылку на функцию мы можем сохранить в переменную и передать куда-нибудь по коду. Так, собственно, и есть, т.к. функции в JavaScript являются разновидностью объекта.

В данном руководстве уже употреблялся термин "событие" и "цикл событий". Также к этим терминам можно добавить объединяющий их "событийно-ориентированное программирование". Именно событийная модель работы Node определяет как такие функции как readFile работают. Сначала Node вызывает функцию readFile и затем ожидает, когда readFile вернет событие завершения чтения файла. Пока Node ожидает это событие, Node может производить другие действия и также ожидать события от других асинхронных вызовов. Внутри Node реализован так называемый "цикл событий", в котором регистрируются все асинхронные вызовы наподобие readFile. В этом цикле Node раз за разом перебирает список всех действующих асинхронных вызовов и проверяет завершился ли каждый из этих вызовов. Как только один из вызовов будет завершен, Node на очередной итерации цикла вызывает закрепленный за данным вызовом колбэк.

Возникает закономерный вопрос - а как отследить последовательность асинхронных вызовов, если они все крутятся в одном цикле событий и какая последовательность завершения каждого из них предсказать довольно тяжело. Представим, что у вас есть 3 асинхронных функции a, b, c. Каждая из них работает по 1 минуте, а затем вызывает переданный в нее колбэк (который идет первым аргументом). Чтобы эти функции выполнились последовательно их необходимо вложить друг в друга:


a(() => {
  b(() => {
    c()
  })
})

При выполнении этого кода a стартует немедленно (в цикл событий заносится информация о том, что выполняется функция a), спустя минуту она завершится и вызовется её колбэк, в котором теперь вызывается b (из цикла событий удаляется информация об a и добавляется о b), которая также спустя минуту завершается, вызывает в свою очередь свой колбэк во второй строке и в нем вызывает наконец функцию c (из цикла событий удаляется информация об b и добавляется о c), и спустя еще одну минуту (из цикла событий удаляется информация об c, а у c нет колбэка поэтому ничего не вызывается) Node завершает свою работу, т.к. обрабатывать или ожидать больше нечего (цикл событий пуст). Таким вложенным кодом можно добиться правильной последовательности асинхронных вызовов. Однако, если увлекаться таким подходом, то можно создать очень высокую степень вложенности, которая часто называется "спагетти код".

Программирование под Node заставляет вас мыслить нелинейно. Представим у нас есть следующий список задач:

  1. Считать файл
  2. Обработать файл

Первое, что приходит на ум это как раз линейный подход:

var file = readFile();
processFile(file);

Но как долго и нудно описывалось выше, предложенный вариант не является Node-путем. Поэтому необходимо сделать так, чтобы processFile вызывался только после успешного окончания чтения файла в readFile. Для этого как раз и созданы колбэки:


const fs = require('fs');

function finishedReading(error, movieData) {
  if (error) return console.error(error);
  // Если ошибки нет, то делаем что-то с movieData
}

fs.readFile('movie.mp4', finishedReading);
Добавить изображение из буфера обмена (Максимальный размер: 20 МБ)