Промисы JavaScript в основных концепциях

Введение

JavaScript – однопоточный язык программирования, поддерживающий асинхронное выполнение кода без блокировки основного потока с помощью промисов.

Введение

Концепция 1: Как создать промис и применить его

Рассмотрим следующую асинхронную функцию:

function async_fn() {
setTimeout(function(){
console.log("executed");
}, 1000);
}

Чтобы async_fn вернула результат своей работы, можно:

  1. Использовать callback-функцию.
  2. Использовать промис.

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

Синтаксис создания нового промиса:

new Promise((resolve, reject)=>{
if(success){
resolve("success");
} else {
reject("failed");
}
});

Пример:

function timer() {
return new Promise((resolve, reject)=> {
setTimeout(()=>{
resolve("Timeout");
},2000);
});
}

Класс Promise предоставляет три метода:

  1. thenесли промис выполняется успешно, то будет осуществлен обратный вызов метода then.
  2. catchесли промис не выполняется, будет осуществлен обратный вызов метода catch.
  3. finallyметод вызывается, если промис выполнен.

Пример использования промиса:

timer().then((response)=>{
// функция, возвращаемая в случае успеха
console.log(response);
}).catch((error)=>{
// функция, возвращаемая в случае ошибки
console.log(error);
}).finally(()=>{
// всегда вызывается
console.log("Promise completed");
});

Концепция 2: Цепочка промисов

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

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

Цепочка вызовов может быть реализована двумя способами:

  • Использование отдельного обработчика ошибок для каждого успешного обратного вызова – при этом метод then принимает два аргумента: один для успешного обратного вызова,а другой - для неудачного:
timer().then(successHandler1, failureHandler1).
.then(successHandler2, failureHandler2)
.then(successHandler3, failuerHandler3)
.then(successHandler4, failureHandler4)
  • Использование стандартного обработчика ошибок – обратный вызов failureHandlerне является обязательным в методе then. Поэтому можно использовать блок catch для общей обработки ошибок:
timer().then(successHandler1)
.then(successHandler2)
.then(successHandler3)
.catch(errorHandler)
Концепция 2: Цепочка промисов

В приведенном выше примере можно передать параметр через разные обратные вызовы и увеличивать их значение.

Если мы возвращаем error из любого обратного вызова then, то для последующих методов then обратный вызов error будет выполнен. После чего ошибка будет передана в последний блок catch.

Концепция 3: Совместное выполнение нескольких промисов

Иногда нужно обработать два или более промисов и запустить обработчик, когда эти промисы будут выполнены. Этого можно добиться двумя способами:

  • Дождитесь разрешения всех промисов: для этого объект Promise предоставляет метод all. Он принимает один или несколько промисов в виде массива и возвращает промис, который разрешается, когда все они выполняются успешно или один из них отклоняется:
Promise.all([promise1, promise2, promise3])
.then(response => {
// ответ - это массив, содержащий результаты переданных промисов
}).catch(error => {
// будет передаваться только первая ошибка. То есть если два промиса приводят к ошибке,  только первая ошибка будет передана в обратный вызов error
})

Все промисы будут выполняться независимо от их результатов. Например, если promise2 вызовет ошибку, тогда promise3 будет выполнен. Затем будет вызван обработчик catch, который передаст ошибку promise2 блоку catch. Но блок then никогда не будет выполнен.

Концепция 3: Совместное выполнение нескольких промисов
  • Дождитесь решения хотя бы одного промиса из списка. Чтобы решить родительский промис, как только выполнится любой из дочерних промисов, можно использовать метод race объекта Promise.
Promise.race([promise1, promise2, promise3])
.then(response => {
// ответ содержит результат первого решенного промиса
}).catch(error => {
// error содержит ошибку  первого решенного промиса
})
Концепция 3: Совместное выполнение нескольких промисов - 2

Концепция 4: Async и await

Async-await позволяет вызывать асинхронные функции синхронно. Рассмотрим этот момент более подробно.

Асинхронные функции – это функции, которые переходят в состояние ожидания при вызове, а разрешение зависит от результата выполнения.

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

Предположим, что у нас есть две асинхронные функции:

  • getManagerByEmployeeId: принимает employeeId в качестве входных параметров и возвращает managerId.
  • getManagerNameById:принимает в качестве входных параметров managerId и возвращает имя менеджера.

Наша задача – получить имя менеджера для данного идентификатора сотрудника. То есть, реализовать функцию getManagerName в приведенном ниже коде.

const EmployeeIDManagerIdMap = {
 "AA234": "0AA316",
 "BBCD5":"4AA354"
};const ManagerIdManagerNameMap = {
 "0AA316":"John Doe",
 "4AA354":"Ravindram S"
};function getManagerByEmployeeId(employeeId) {
  return new Promise((resolve, reject)=> {
    setTimeout(()=>{
      if(EmployeeIdManagerIdMap[employeeId]) {
       resolve(EmployeeIdManagerIdMap[employeeId]);
      } else {
       reject(`Invalid employee id ${employeeId}`);
      }
    },2000);
  });
}function getManagerNameById(managerId) {
    return new Promise((resolve, reject)=> {
    setTimeout(()=>{
      if(ManagerIdManagerNameMap[managerId]) {
       resolve(ManagerIdManagerNameMap[managerId]);
      } else {
       reject(`Invalid manager id ${managerId}`);
      }
    },2000);
  });
}// получаем имя менеджера по employeeId
function getManagerName(employeeId) {
  // возвращаем имя менеджера
}

Один из способов сделать это – использовать структуру вложенных промисов и преобразовать getManagerName в промис.

function getManagerName(employeeId){
return new Promise((resolve, reject)=>{
getManagerByEmployeeId(employeeId).then(function(managerId){
getManagerNameById(managerId).then(function(managerName){
resolve(managerName);
}, function(error){
reject(error);
})
}, function(error){
reject(error);
})
})
}

Это создаст структуру вложенного кода, которая сложна для понимания и заставляет использовать промис в функции getManagerName.

Теперь рассмотрим реализацию концепции async-await.

async function getManagerName(employeeId){
try {
let managerId = await getManagerByEmployeeId(employeeId);
try {
let managerName = await getManagerNameById(managerId);
return managerName;
} catch(error) {
console.error("getManagerNameById promise rejected", error);
}
} catch(error){
console.error("getManagerByEmployeeId promise rejected", error);
}
}

Благодаря ей код стал проще и нам не нужно использовать промис в функции getManagerName.

Конструкция await откладывает выполнение кода до тех пор, пока не будет разрешена вызываемая асинхронная функция.

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

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

Концепция 5: Использование разрешенных промисов

Во время модульного тестирования иногда нужно имитировать действие фактического промиса. Мы можем сделать это, используя уже разрешенные промисы с помощью методов resolve и reject класса Promise.

// Аналогичным образом вы можете также использовать метод reject
const getEmployeeData = Promise.resolve({"name":"Piyush", "age":"25", "ssn":"1324ssb"});testEmployeeSuccess = function(){
  // промис уже разрешен
 getEmployeeData().then(function(employeeData){
    var result = processData(employeeData); 
    if(testResult(result)){
      console.log("test OK");
    } else {
      console.log("test failed");
    }
 });
}

Заключение

Чтобы узнать о промисах еще больше, изучите соответствующую документацию MDN.