В node.js (да и в самом javacript) делается ставка на асинхронность. Поэтому перебежчикам из других языков поначалу ход выполнения программы может показаться странным и даже нелогичным. Ещё бы — любая вызванная функция выполняется в фоновом потоке и сразу после вывода отдаёт управление следующей строчке кода. Справедливости ради скажем, что иначе и быть не могло — javascript пока медленнее чем любой native язык, поэтому распараллеливание обработки конечно ему просто необходимо.

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

Чтобы решить все эти проблемы, в javascript существует механизм promise. По сути — это просто установка семафора окончания фонового потока. Вот как это работает:

  1. Сначала мы объявляем (создаём) этот семафор.
  2. ПОСЛЕ завершения действия фонового потока, мы включаем семафор.
  3. В совершенно произвольном месте кода мы создаём обработчик семафора.

Подробнее про различные классы промизов (и прочие особенности) можно почитать тут, однако пока обойдёмся без такого погружения.

По некоторым соображениям поддержка promise в node.js была убрана, однако появились сторонние реализации — библиотеки q, when и так далее. По результатам бенчмарка самым быстрым был признан Vow — его и рассмотрим.

Страница на github, страница на nodejsmodules.

Установка пакета Vow:

npm install vow
  1. Создание семафора:
var vow = require('vow');
var semaphore1 = vow.promise();
var semaphore2 = vow.promise();
  1. Включение семафора:
//В одной асинхронной функции
semaphore1.fulfill(0);

//В другой асинхронной функции
semaphore2.fulfill(0);
  1. Обработка семафора:
vow.all([semaphore1,semaphore2]).then(function(value) {
  //обработка
});

Ничего сложного в этом нет. Надо сказать, что в функции включения семафора можно передавать некое значение, или объект. Тогда функцию then лучше заменить на spread — она более удобно представляет значения промизов в обработчик. Поясню на примере.

Типичное для меня применение промизов — параллельная загрузка файла-шаблона и выборка из базы данных значений для заполнения этого шаблона. Тогда с помощью промизов всё можно сделать так:

var file_read = vow.promise();
var sql_select = vow.promise();

fs.readFile('template.htm', function (err,page){ file_read.fulfill(String(page)); });

connection.query('select * form table;', function(fields, result) {
    sql_select.fulfill(photos);
});

vow.all([file_read, sql_select]).spread(function (page, photos) {
    //конечно, за кадром осталась работа с шаблоном и данными - она не нужна
    //для демонстрации. Примем, что мы как бы просто склеиваем две этих строки.
    res.end(page+photos);
});

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

Более глубокое использование vow.js

Выше был рассмотрен самый простой случай использования vow.js, продвинемся немного глубже. У промизов есть два метода завершения — fulfill(), который сигнализирует об успешном завершении, и reject() — о неуспешном. Их «ИЛИ» объединение — сигнал resolved() . Также промиз может уведомить материнский процесс о чём-то, не сигнализируя о завершении, для этого используется метод notify. Использование всех этих методов позволяет гибко выстраивать алгоритм работы с ними, не прибегая к передаче какой-то информации в теле ответа промиза — и не тратя время и код на разбор этой информации в материнском процессе.

Методы работы с сигналами промизов в vow.js.

Для «измерения» сигнала — функция valueOf(), которая отвечает fulfilled, rejected или undefined если работа потока ещё не завершена. Аналоги — функции isFulfilled(), isRejected(), и объединяющая их по принципу «ИЛИ» функция isResolved().

switch-функция then(). В зависимости от ответа промиза вызывает одну из трёх внутренних функций — onFulfilled (){}, onRejected(){}, или onNotified(){}. Сокращает код да и вообще делает его немного читабельнее. Похожие на неё функции fail(), которая вызывает внутреннюю функцию в случае ответа reject, и always() — которая срабатывает на ответ resolved, т.е. fulfilled   rejected.

progress() — удобная функция, реагирующая на сигнал notify. Позволяет отображать, например, прогресс выполнения промиза — да и вообще всё что вы передадите в качестве уведомления.

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