Асинхронное управление потоками при помощи async

Код контроллера для некоторых страниц библиотеки будет зависеть от результатов многих асинхронных запросов, которые должны выполняться в определённом порядке или параллельно. Для того, чтобы управлять потоком выполнения, и выводить страницы, когда получена вся необходимая информация, будет использован async - известный модуль node.

Примечание: В JavaScript существует много других способов управления асинхронным поведением и потоком выполнения, включая такой относительно новый элемент языка JavaScript как промисы (promises).

Модуль Async имеет массу полезных методов (см. документацию the documentation). Вот некоторые наиболее важные функции:

  • async.parallel() для осуществления любых операций, которые должны выполняться параллельно.
  • async.series() если нужно иметь уверенность, что асинхронные операции выполняются последовательно.
  • async.waterfall() для операций, которые должны выполняться последовательно, причём каждая операция зависит от результатов предыдущих операций.

Почему это необходимо?

Большинство методов, которые используются в Express - асинхронные - вы определяете выполняемую операцию, передавая колбэк-функцию. Метод завершается немедленно, а колбэк-функция вызывается тогда, когда завершилась запрошенная операция. По соглашению, принятому в Express, колбэк-функция передаёт значение ошибки error как первый параметр (или null при успехе) и результат функции (если есть) как второй параметр.

Если контроллер должен выполнить только одну асинхронную операцию, чтобы получить информацию для представления страницы, то реализация проста - мы просто представляем шаблон в колбэке. Фрагмент кода (ниже) демонстрирует это для функции, которая подсчитывает количество элементов модели SomeModel (применяя метод Mongoose count() ):

exports.some_model_count = function(req, res, next) {

  SomeModel.count({ a_model_field: 'match_value' }, function (err, count) {
    // ... сделать что-то, если ошибка

    // При успехе представить результат, передав count в render-функцию (здесь - как переменную 'data').
    res.render('the_template', { data: count } );
  });
}

Однако что, если требуется сделать множественные асинхронные запросы, и результат нельзя представить, пока не завершились все операции? Наивная реализация могла бы использовать "венок" запросов, запуская последующие запросы в колбэках предыдущих, и представляя ответ в последнем колбэке. Проблема такого подхода состоит в том, что запросы должны выполняться последовательно, хотя, вероятно, было бы более эффективно выполнять их параллельно. Это также может привести к усложнённому вложенному коду, что обычно называют адом колбэков ( callback hell ).

Намного лучше было бы выполнять все запросы параллельно, и иметь единственную колбэк-функцию, которая будет вызвана после того как все запросы выполнены. Именно такое выполнение операций модуль Async делает лёгким и простым!

Параллельные асинхронные операции

Метод async.parallel() используется для параллельного выполнения нескольких асинхронных операций.

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

Возможный второй аргумент для async.parallel() - это callback -функция, которая должна быть вызвана после завершения всех функций, указанных в первом аргументе. Эта функция вызывается с аргументом ошибки и результатом - коллекцией результатов отдельных асинхронных операций. Тип коллекции - такой же, как и тип первого аргумента async.parallel (т.е. если передаётся массив асинхронных функций, итоговая колбэк-функция будет вызвана с массивом результатов). Если любая из параллельных функций сообщила об ошибке, сразу вызывается итоговая колбэк-функция, которая возвращает ошибку.

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

async.parallel({
  one: function(callback) { ... },
  two: function(callback) { ... },
  ...
  something_else: function(callback) { ... }
  },
  // optional callback
  function(err, results) {
    // 'results' равны: {one: 1, two: 2, ..., something_else: some_value}
  }
);

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

Последовательные асинхронные операции

Для выполнения нескольких асинхронных операций последовательно используется метод async.series() , при этом последующие функции не зависят от результатов предыдущих функций. Метод определяется и ведёт себя так же, как и async.parallel().

async.series({
  one: function(callback) { ... },
  two: function(callback) { ... },
  ...
  something_else: function(callback) { ... }
  },
  // optional callback after the last asynchronous function completes.
  function(err, results) {
    // 'results' is now equals to: {one: 1, two: 2, ..., something_else: some_value}
  }
);

Примечание: Спецификация языка ECMAScript (JavaScript) устанавливает, что порядок в перечислении объектов не определён, поэтому возможно, что функции не будут вызываться в том порядке, в котором вы их задали на всех платформах. Если порядок вызова действительно важен, вместо объекта следует передавать массив, как показано ниже.

async.series([
  function(callback) {
    // do some stuff ...
    callback(null, 'one');
  },
  function(callback) {
    // do some more stuff ...
    callback(null, 'two');
  }
 ],
  // optional callback
  function(err, results) {
  // results is now equal to ['one', 'two']
  }
);

Последовательные зависимые асинхронные операции

Выполнение нескольких асинхронных операций последовательно, когда каждая операция зависит от результатов предыдущих операций, осуществляется методом async.waterfall().

Функции-callback, которая вызываются асинхронными функциями , содержит null как первый аргумент, и результаты в следующих аргументах. Каждая функция в последовательности (кроме первой) как аргументы использует результаты предыдущих функция, а колбэк-функция является последним аргументом. Когда операции завершаются, вызывается финальная колбэк-функция, аргументы которой - объект err и результат последней операции. Как это работает, станет более ясным после рассмотрения примера - фрагмента кода, приведённого ниже ( пример взят из документации async):

async.waterfall([
  function(callback) {//первая функция в цепочке
    callback(null, 'one', 'two');//результаты 'one' и 'two'
  },
  function(arg1, arg2, callback) {//вторая функция в цепочке
    // arg1 равен 'one' , arg2 равен  'two'
    callback(null, 'three'); //результат 'three'
  },
  function(arg1, callback) {
    // arg1 равен 'three'
    callback(null, 'done'); //результат 'done'
  }
], function (err, result) {
  // result равен 'done'
}
);

Установка async

Установим модуль async при помощи менеджера пакетов NPM, чтобы использовать его в своём коде. Это делается обычным способом - откроем окно команд в корне проекта LocalLibrary и введём команду:

npm install async

Дальнейшие шаги