Java Script

Если вы не совсем в курсе современных тенденций JavaScript, то, по крайней мере, слышали о промисах ранее, но не знаете, где и как их можно было бы применить.

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

Проблема обратных вызовов

Хотя промисы и существуют уже на протяжении нескольких лет, давайте все же вспомним времена, когда их не было. Это поможет лучше понять, почему они являются хорошим способом управления асинхронными операциями.

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

function loadScript(src, callback) { 
   let script = document.createElement('script');
   script.src = src;
   script.onload = function() { 
     callback(src); 
   };
   document.body.appendChild(src);
}

loadScript('some-script.js', function(src) {
   console.log('The script is loaded');

Как вы видите, функция loadScript передается параметром обратного вызова, который выполняется после загрузки скрипта. Все это работает прекрасно, но возникает один большой вопрос: “Что случится, если скрипт по какой-либо причине не загрузится?”. Со стороны разработчика будет наивным игнорировать такую возможность.

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

loadScript('some-script.jpg', function(src, error) {
   if (error) {
      // Обработка ошибки
   } else { 
      console.log('The script is loaded');
   }   
})

loadScript('some-script.jpg', function(src, error) {
   if (error) {
      // Обработка ошибки
   } else { 
      console.log('The script is loaded');
   }   
})

Но что, если у нас будет ситуация, в которой мы будем зависеть от загрузки не одного изображения, а, например, трех или более?

Именно такие ситуации и приводят к возникновению кошмара в обратных вызовах. 

loadScript('first-script.js', function(src, error) {
   loadScript ('second-script.js', function(src, error) {
      loadScript ('last-script.js', function(src, error) {
        ...
      })   
   }) 
})

var promise = new Promise(function(resolve, reject) {
   // Асинхронная операция, запрашивающая данные от API
   var data = fetchApiData()
  
  if (data.http_status === 200) {
    resolve("The data was succesfully fetched");
  } else {
    reject("Something went wrong..");
  }
});

Краткое введение

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

“Объект Promise представляет окончательное выполнение (или провал) асинхронной операции, а также ее итоговое значение.”

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

Промис может иметь одно из четырех состояний:

  1. fulfilled: выполненный;
  2. rejected: отклоненный;
  3. pending: не выполнен и не отклонен;
  4. settled: давно выполнен. Это не настоящее состояние, а фигура речи.

Промисы в коде

Как было отмечено ранее, они главным образом применяются для управления асинхронными операциями наподобие API вызовов или файловых операций. Все эти операции инициируются внутри функции executor.

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

new Promise(executor);
new Promise(function(resolve, reject) { ... });

Становится ясно, что конструктор Promise принимает функцию, называемую executor. У этой функции есть два параметра: resolve и reject, которые также являются функциями.

Теперь давайте создадим простой пример промиса для лучшего понимания:

var promise = new Promise(function(resolve, reject) {
   // Асинхронная операция, запрашивающая данные от API
   var data = fetchApiData()
  
  if (data.http_status === 200) {
    resolve(data);
  } else {
    reject("Something went wrong..");
  }
});

В этом примере мы пробуем запросить данные из API. Если API вернет HTTP код состояния 200, значит запрос оказался успешным. В противном случае он провалился.

Другими словами, если HTTP код состояния 200, то промис resolved. В ином другом случае rejected. 

Как же нам применять промис, который мы только что создали?

promise.then(function(result) { 
  /* добавить результат в список */
}).catch(function() {
  /* что-то пошло не так, отобразить сообщение об ошибке пользователю */
}).finally(function() {
  /* выполняется независимо от успеха или провала */ 
});

Когда была вызвана функция resolve или reject, промис считается завершенным (settled). С этого момента начинается выполнение следующего звена в цепочке. Обычно это then или catch. Имейте в виду, что одним executor может быть вызвана только одна функция resolve или reject. После их выполнения состояние промиса окончательно меняется на завершенное и дальнейшие вызовы resolve или reject будут проигнорированы. 

Когда промис выполнен успешно, активируется обратный вызов then. Вы также можете создавать цепочки вызовов методов then. Если же промис был отклонен, активируется обратный вызов catch. finally вызывается независимо от успеха или провала выполнения.

All и Race

Последние две функции, которые мы еще не рассмотрели — это all и race. Они полезны, но их используют реже остальных. 

All

Идеальным примером применения Promise.all может выступить одновременная множественная отправка AJAX запросов.

var request1 = fetch('/users.json');
var request2 = fetch('/products.json');

Promise.all([request1, request2]).then(function(results) {
  // Оба промиса выполнены
});

Race

Вместо ожидания завершения всех промисов, Promise.race запускается, как только оказывается выполнен или отклонен хотя бы один промис в цепочке. 

var request1 = new Promise(function(resolve, reject) { 
  setTimeout(function() { resolve('First!'); }, 5000);
});

var request2 = new Promise(function(resolve, reject) { 
  setTimeout(function() { resolve('Second!'); }, 1000);
});

Promise.race([request1, request2]).then(function(output) {
  console.log('Output: ', output);
});

Выводом в консоли будет: “Output: Second!”. Обратите внимание, что промис request1 никогда не будет resolved.

Читайте также:


Перевод статьи Daan: An Introduction to Promises in JavaScript