Я всегда придерживался своих асинхронных предпочтений, используя обратные вызовы и другие исправления как знак чести: никаких обещаний для меня я бы злорадствовал с гордостью. Идиот. На самом деле обещания велики - на самом деле для современного приложения Node / Express они равносильны важности.

Проблема, которую необходимо решить, - это проблема асинхронных / асинхронных операций. То есть, говоря простым языком, когда операция или функция начинает выполняться, но по какой-либо причине имеет некоторую задержку до завершения. Примеры этого включают HTTP-запрос, такой как запросы AJAX, и функции setTimeout. Эти операции начинаются, а затем заканчиваются, когда возвращается ответ или когда заканчивается таймер. Пока компьютер ожидает завершения этих операций, он продолжает заниматься следующими строками кода. Хорошо, что вы говорите, держите его занятым, но это вызывает серьезную проблему - любой код, зависящий от предыдущего асинхронного кода, может быть запущен до того, как этот асинхронный код будет завершен, что означает ошибки. Видеть:

var data = makeAsyncRequest();
console.log("Data is " + data); // Data is undefined

Вышеупомянутое происходит потому, что асинхронный запрос, например запрос AJAX / HTTP, еще не вернул ответ. Запрос все еще где-то в мире, где-то разговаривает с другими компьютерами. Тем временем компьютер продолжает работу и пытается console.log переменную данных, которой пока не присвоено никакого значения. Какая дилемма, да! К счастью, есть несколько решений, которые мы можем рассмотреть.

1) Обратные вызовы

Функция обратного вызова - это функция, предоставляемая в качестве аргумента другой функции, которая выполняется после завершения асинхронной операции. Это обычное дело в JavaScript. Например:

fs.readFile("file.name", function(err, data){ //callback
   if(err) throw err;
   console.log(data);
})

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

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

Можно сказать, что у обратных вызовов есть свое место, и они очень полезны для множества задач JavaScript, но они далеки от идеи в этих многоуровневых сценариях.

2) Модули промежуточного программного обеспечения

Используя Express.js, у нас есть еще один вариант: использовать промежуточное ПО. ПО промежуточного слоя Express принимает 3 аргумента, req, res, next. Затем обратный вызов автоматически передается в каждый модуль промежуточного программного обеспечения, поэтому мы можем добиться эффекта решения обратного вызова без беспорядочных отступов.

Следовательно, мы можем сделать асинхронный запрос с одним промежуточным программным обеспечением, и только после того, как этот запрос ответит, мы сохраняем эти данные в req.something, вызываем next() и переходим к следующему промежуточному программному обеспечению для обработки полученных данных.

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

  • req становится свалкой данных. req - это поток, предназначенный для передачи данных о HTTP-запросе, который его инициировал.
  • Поскольку функции требуют, чтобы данные были помещены в req, а затем использовались в следующем промежуточном программном обеспечении, это делает каждый модуль практически не пригодным для повторного использования, что полностью исключает возможность использования модулей.
  • Смешивать контроллеры (маршруты и их промежуточное ПО) с моделями (взаимодействие с базами данных и т. Д. - асинхронные операции) - просто плохая практика. Это также делает практически невозможным модульное тестирование.

Тогда здесь не повезло. Промежуточное ПО - это не ответ.

Обещания и ожидание: ответ на наши проблемы с асинхронностью

Мы хотим, чтобы наши асинхронные операции вели себя так же, как синхронные операции. Но мы также не хотим, чтобы наш код останавливался в ожидании возврата из асинхронной операции. Решение: обещает.

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

Вот как можно использовать обещания:

var asyncRequest = new Promise(function(resolve, reject){
  return setTimeout(function(){
      resolve("Here's your data");
    }, 3000);
    reject("Error")
})

Обещание создается с помощью конструктора Promise. Конструктор принимает один аргумент - функцию, которая принимает в качестве параметров resolve & reject. Как только ваши данные будут возвращены, вы можете вызвать resolve, передав данные, с которыми вы хотите работать позже. Если есть ошибка, вместо этого вызовите отклонение.

Вызов любой функции завершает обещание и запускает обработчики .then или .catch. Как уже упоминалось, это обработчики событий, которые отвечают на заключительное обещание.

makeAsyncRequest.then(function(val){
  console.log(val) 
}, function(val){
  throw val;
})
// the above and below have the same effect
makeAsyncRequest.then(function(val){
  console.log(val) 
}).catch(function(val){
  throw val;
})

.then принимает два параметра: сначала функцию, вызываемую, если обещание вызывает resolve(), и, во-вторых, функцию, вызываемую, если обещание вызывает reject(). В качестве альтернативы вы можете использовать .catch для перехвата reject() вызовов, как показано ниже.

Каждая из функций, переданных в then и catch, принимает один аргумент, который будет заполнен как аргумент, переданный в resolve или reject соответственно. Таким образом, нижеследующее также будет работать:

makeAsyncRequest.then(console.log).catch(function(val){
  throw val;
})

Замечательно. Давайте посмотрим, как это может работать с Express.

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

app.get("/getdata", function(req, res){  
   pullData().then(function(data){
      return filterByYear(data);
   }).then(function(filteredData){
      res.json(filteredData);
   })
})

Предполагая, что pullData() & filterByYear() возвращают обещания, мы объединили вместе .then вызовов, чтобы позволить нам выполнять несколько асинхронных операций одну за другой.

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

Фантастика. Но мы можем сделать даже лучше.

Использование Await

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

Ключевое слово await может использоваться, чтобы указать, что следующая функция будет возвращать обещание, которое следует ожидать перед выполнением любого другого зависимого кода. await можно использовать только внутри async function.

app.get("/getdata", async function(req, res){  
   var data = await pullData();
   var filteredData = await filterByYear(data);
   res.json(filteredData);
})

Преимущество await в том, что асинхронный код может выглядеть точно так же, как синхронный JavaScript. Посмотрите, какой изящный этот код. Не увеличивая async & await ключевые слова, вы вряд ли заметите, что это не обычный код. Опять же, при условии, что функции pullData & filteredData возвращают обещания, любая функция, зависящая от ожидаемой функции, не будет выполняться, пока обещание функции не будет выполнено.

Это обеспечивает аккуратную многоразовую модульность и невероятную ясность.

Модульное тестирование асинхронных модулей

С await модульное тестирование асинхронных модулей в Mocha / Chai просто, и нет необходимости вызывать done(). Просто сделайте функцию, переданную it, async функцией, а await ответом модуля, прежде чем использовать expect.

describe("getData", function(){ 
   it("Data should be 101 indices long", async function(){
       var data = await getData(testData);
       expect(data.length).to.equal(101); 
   });
});

И вуаля…

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

На мой взгляд, функциональность Promises и await / async изменила приложения Node.js больше, чем любое другое введение в платформу, и трудно вспомнить, что мы когда-либо делали до них!

Наслаждаться.