[Из песочницы] Promises 101
Перевод первой части отличной статьи про промисы. Базовые приемы создания и управления промисами.
Промисы используются для операций, вычисление которых занимает неопределенное время. Примером подобной операции может быть сетевой запрос, когда мы запрашиваем данные у API и не можем точно определить, когда будет получен ответ.
Если есть другие операции, выполнение которых зависит от этого сетевого запроса, то вырисовывается проблема. Без промисов нам придётся использовать вереницу колбэков (callbacks), чтобы выстроить последовательность операций. Это нормально, если у нас одно асинхронное действие. Но если нужно сделать несколько последовательных асинхронных шагов, колбэки становятся неуправляемыми и результат печально известен как лапша колбеков (callback hell)
doSomething(function(responseOne) {
doSomethingElse(responseOne, function(responseTwo, err) {
if (err) { handleError(err); }
doMoreStuff(responseTwo, function(responseThree, err) {
if (err) { handleAnotherError(err); }
doFinalThing(responseThree, function(err) {
if (err) { handleAnotherError(err); }
// Выполнено
}); // конец doFinalThing
}); // конец doMoreStuff
}); // конец doSomethingElse
}); // конец doSomething
Промисы предоставляют стандартизированный и понятный метод решения задач, которые должны выполняться последовательно.
doSomething()
.then(doSomethingElse)
.catch(handleError)
.then(doMoreStuff)
.then(doFinalThing)
.catch(handleAnotherError)
Создание промисов
Промисы создаются при помощи конструктора промисов. Он представляет собой функцию с двумя аргументами (resolve
& reject
) в качесте параметров.
var promise = new Promise(function(resolve, reject) { /* Содержимое промиса */ } )
Внутри этой функции мы можем выполнять любые асинхронные задачи. Чтобы отметить промис как исполненный, мы вызываем resolve()
, передавая ему значение, которое мы хотим возвратить. Что бы отметить промис как отклонённый или неудачный, мы вызываем reject()
, передавая ему сообщение ошибки. До того, как промис станет исполненным или отклоненным, он находится в состоянии ожидания.
Вот промис версия XMLHttpRequest -
/* CREDIT - Jake Archibald, http://www.html5rocks.com/en/tutorials/es6/promises/ */
function get(url) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
if (req.status == 200) {
resolve(req.response); /* ПРОМИС ВЫПОЛНЕН */
} else {
reject(Error(req.statusText)); /* ПРОМИС ОТКЛОНЁН */
}
};
req.onerror = function() { reject(Error("Network Error")); };
req.send();
});
}
Использование промисов
Что бы выполнить промис, мы можем вызвать его как любую обычную функцию. Но, так как это промис, у нас есть доступ к .then
методу, который мы можем добавить к функции и который будет исполнен когда промис выйдет из режима ожидания.
.then()
метод принимает два необязательных параметра. Первый — это функция, которая вызывается, когда промис исполнен (resolved). Второй — функция, которая выполняется если промис отклонён (rejected).
get(url)
.then(function(response) {
/* successFunction */
}, function(err) {
/* errorFunction */
})
Обработка ошибок
Так как оба параметра (successFunction и errorFunction) опциональны, мы можем разделить их на два .then()
для лучшей читаемости.
get(url)
.then(function(response) {
/* successFunction */
}, undefined)
.then(undefined, function(err) {
/* errorFunction */
})
Что бы сделать код еще более понятным, мы можем использовать .catch()
метод, который является сокращенным вариантом для .then(undefined, errorFunction)
get(url)
.then(function(response) {
/* successFunction */
})
.catch(function(err) {
/* errorFunction */
})
Формирование цепи
Настоящая ценность промисов заключается в том, что мы можем выполнять несколько асинхронных функций по порядку. Мы можем объединить .then()
и .catch()
вместе для создания последовательности асинхронных функций.
Мы можем сделать это, возвращая еще один промис после выполнения или отклонения предыдущего. Например -
get(url)
.then(function(response) {
response = JSON.parse(response);
var secondURL = response.data.url
return get( secondURL ); /* Возвращаем новый промис */
})
.then(function(response) {
response = JSON.parse(response);
var thirdURL = response.data.url
return get( thirdURL ); /* Возвращаем новый промис */
})
.catch(function(err) {
handleError(err);
});
Если промис исполнен (resolved), то вызовется ближайший .then()
в последовательности. Если промис отклонён (rejected), то ближайший .catch()
в последовательности.
Паралелльное выполнение промисов
Может возникнуть ситуация, когда нам понадобится выполнить несколько промисов параллельно, и продолжать алгоритм только после того, как все промисы будут выполнены. Например, если мы хотим получить ряд изображений и только после этого отобразить их на странице.
Чтобы это сделать, нам необходимо использовать два метода. Это Array.map()
, для того, что бы применить промис для каждого элемента массива и сохранить результат в новый массив. И Promise.all()
, который выполнит resolve()
в случае исполнения всех промисов в массиве. Если хоть один промис в массиве будет отклонен, Promise.all()
тоже будет отклонён.
var arrayOfURLs = ['one.json', 'two.json', 'three.json', 'four.json'];
var arrayOfPromises = arrayOfURLs.map(get);
Promise.all(arrayOfPromises)
.then(function(arrayOfResults) {
/* Сделать что-нибудь, когда все промисы в массиве зарезолвятся */
})
.catch(function(err) {
/* Выполняется, если хоть один промис в массиве отклонён */
})
Если мы посмотрим в сетевую панель (Network panel) инструментов разработки (Development tools), мы увидим, что все запросы случаются параллельно.
Если вам нужна поддержка IE и/или Opera Mini, используйте полифил.
Спасибо за внимание!