Этот материал мы подготовили для JavaScript-программистов, которые только начинают разбираться с «Promise». Обещания (promises) в JavaScript – это новый инструмент для работы с отложенными или асинхронными вычислениями, добавленный в ECMAScript 2015 (6-я версия ECMA-262).
До появления «обещаний» асинхронные задачи можно было решать с помощью функций обратного вызова или с помощью обработки событий. Универсальный подход к решению асинхронных задач – обработка событий. Менее удобный, но также имеющий право на существование, способ использовать функции обратного вызова. Конечно, выбор решения зависит от стоящей перед вами задачи. Вариант решения задач с помощью «обещаний», скорее, призван заменить подход к функциями обратного вызова.
В использовании функций обратного вызова есть существенный недостаток с точки зрения организации кода: "callback hell". Этот недостаток заключается в том, что в функции обратного вызова есть параметр, который, в свою очередь, также является функцией обратного вызова – и так может продолжаться до бесконечности.
Может образоваться несколько уровней таких вложенностей. Это приводит к плохому чтению кода и запутанности между вызовами функций обратного вызова. Это, в свою очередь, приведет к ошибкам. С такой структурой кода найти ошибки очень сложно.
Если все же использовать такой подход, то более эффективно будет инициализировать функции обратного вызова отдельно, создавая их в нужном месте.
Давайте рассмотрим работу «обещаний» на примере конкретной задачи:
После загрузки страницы браузера необходимо показать изображения из указанного списка.
Список представляет собой массив, в котором указан путь к изображению. Например, для показа изображений в слайдере вашей баннерной системы на сайте или асинхронной загрузки изображений в фотоальбоме.
/**
* список изображений
* (предположим, что изображения 1.jpg, 2.jpg, 3.jpg, 4.jpg существуют, а
* fake.jpg - нет)
*
* @type {string[]}
*/
var imgList = ["img/1.jpg", "img/2.jpg", "img/fake.jpg", "img/3.jpg", "img/4.jpg"];
Сначала напишем функцию, которая подгружает одно изображение по указанному url.
function loadImage(url)
/**
*
* подгружаем изображение по указанному url
*
* @param url
* @returns {Promise}
*/
function loadImage(url)
{
//объект "обещание"
return new Promise(function(resolve, reject)
{
var img = new Image();
img.onload = function()
{
//в случае успешной загрузки изображения, результат "обещания" будет url этого изображения
return resolve(url);
}
img.onerror = function()
{
//в случае не успешной загрузки изображения, результат "обещания" будет url этого изображения
return reject(url);
}
img.src = url;
});
}
Объект «обещание» создается с помощью конструктора new Promise(...), которому в качестве аргумента передается анонимная функция с двумя параметрами: resolve, reject. Они, в свою очередь, так же являются функциями. Resolve() — сообщает о том, что код выполнен «успешно», reject() – код выполнен с «ошибкой» (что считать «ошибкой» при выполнении вашего кода, решать вам. Это что-то вроде if(true){...} else {...}).
Интерфейс Promise (обещание) представляет собой обертку для значения, неизвестного на момент создания обещания. Он позволяет обрабатывать результаты асинхронных операций так, как если бы они были синхронными: вместо конечного результата асинхронного метода возвращается обещание, результат которого можно получить в некоторый момент в будущем.
При создании обещание находится в ожидании (состояние pending), а затем может стать выполнено (fulfilled), вернув полученный результат (значение), или отклонено (rejected), вернув причину отказа.
В методы resolve() и reject() можно передавать любые объекты. В метод reject(), как правило, передают объект типа Error с указанием причины ошибки («отклоненного» состояния «обещания»). В любом случае, это не обязательно. Решение, как дальше вы будете обрабатывать такие ситуации – за вами.
На данный момент может показаться, что «обещание» совершенно не нужно использовать в этой ситуации. Пока мы лишь устанавливаем некий индикатор того, было ли подгружено изображение. Однако вскоре вы увидите, что этот механизм может легко, интуитивно понятно определять, что произойдет после того, как задача будет выполнена (изображение подгружено или нет).
Методы then() и catch()
Всякий раз, когда вы создаете объект «обещание», становятся доступны два метода: then() и catch(). Используя их, вы можете выполнить нужный код при успешном разрешении «обещания» (resolve(...)) или же код, обрабатывающий ситуацию с «ошибкой» (reject(...)).
then() и catch()
Примечание: не обязательно возвращать (return) resolve(...) или reject(...):. В примере выше можно было бы написать так:
function myPromise()
{
return new Promise(function(resolve, reject)
{
//псевдо асинхронный код
var ascync = true; //или false
if (!ascync)
return reject(new Error("не удалось выполнить..."));
return resolve(1);
});
}
myPromise()
.then(function(res)
{
console.log(res); //выведет 1
})
.catch(function(err){
console.log(err.message); //выведет сообщение "не удалось выполнить..."
});
Примечание: не обязательно возвращать (return) resolve(...) или reject(...):. В примере выше можно было бы написать так:
//псевдо асинхронный код
var ascync = true; //или false
if (!ascync)
{
reject(new Error("не удалось выполнить..."));
}
else
{
resolve(1);
}
В результате вызова myPromise() все равно сработал бы метод then() или catch(). Лучше всего завести сразу привычку — всегда возвращать resolve(...) или reject(...). В будущем это поможет избежать ситуации, когда код будет работать не так, как ожидается.
В методы then() и catch() передают две анонимные функции. Синтаксис метода then() в общем случае такой:
then(function onSuccess(){}, function onFail(){});
Параметр function onSuccess(){} будет вызван в случае успешного выполнения «обещания», function onFail(){} – в случае ошибки. По этой причине следующий код будет работать одинаково:
Примеры метода then()
myPromise()
.then(function(res)
{
console.log(res); //выведет 1
})
.catch(function(err){
console.log(err.message); //выведет сообщение "не удалось выполнить..."
});
myPromise()
.then(function(res)
{
console.log(res); //выведет 1
},
function(error)
{
console.log(err.message); //выведет сообщение "не удалось выполнить..."
});
myPromise()
.then(function(res)
{
console.log(res); //выведет 1
})
.then(undefined, function(error)
{
console.log(err.message); //выведет сообщение "не удалось выполнить..."
});
Гораздо привычнее и понятнее использовать catch(...). Также метод catch() можно вызывать «посередине» цепочки вызовов then(), если логика вашего кода того требует: then().catch().then().Не забывайте вызывать catch() последним в цепочке: это позволит вам всегда отлавливать «ошибочные» ситуации.
Вызовем наш метод loadImage(url) и для примера добавим одну картинку на страницу:
//считаем, что на странице есть элемент с id="images", например, div
loadImage(imgList[0])
.then(function(url)
{
$('#images').append('<img src="'+url+'" style="width: 200px;" />');
})
.catch(function(url)
{
//как и сообщалось выше, не обязательно, чтобы сюда передавался объект типа Error
//например, вы захотите сохранить в отдельный массив пути к картинкам , которые не подгрузились, и потом что-нибудь с ним сделать...
console.log("не удалось загрузить изображение по указанному пути: ", url);
});
Последовательная рекурсивная подгрузка и отображение изображений
Напишем функцию для последовательного отображения изображений:
function displayImages(images)
/**
* последовательная рекурсивная подгрузка и показ изображений
*
* @param images - массив с url
*/
function displayImages(images)
{
var imgSrc = images.shift(); // проходим по массиву с изображениями
if (!imgSrc) return; //если в результате рекурсии прошлись по всему массиву
//если в массиве еще есть изображение, загружаем его
return loadImage(imgSrc)
.then(function(url)
{
$('#images').append('<img src="'+url+'" style="width: 200px;"/>');
return displayImages(images); //рекурсия
})
.catch(function(url)
{
//если какое-то из изображений не загрузилось, переходим к следующему изображению
console.log('не удалось загрузить изображение по указанному пути: ', url);
return displayImages(images); //рекурсия
});
}
Функция displayImages(images) последовательно проходит по массиву с url изображений. В случае успешной подгрузки мы добавляем изображение на страницу и переходим к следующему url в списке. В противоположном случае – просто переходим к следующему url в списке.
Возможно, такое поведение отображения изображений не совсем то, что необходимо в данном случае. Если требуется показать все изображение только после того, как они были загружены, нужно реализовать работу с массивом «обещаний».
var promiseImgs = [];
promiseImgs = imgList.map(loadImage);
//для наглядности
promiseImgs = imgList.map(function(url){
return loadImage(url);
});
В массиве promiseImgs теперь находятся «обещания», у которых состояние может быть как «разрешено» так и «отклонено», так как изображения fake.jpg физически не существует.
Для завершения задачи можно было бы воспользоваться методом Promise.all(...).
Promise.all(iterable) возвращает обещание, которое выполнится после выполнения всех обещаний в передаваемом итерируемом аргументе.
Однако у нас в списке есть изображение, которого физически не существует. Поэтому методом Promise.all воспользоваться нельзя: нам необходимо проверять состояние объекта «обещание» (resolved | rejected).
Если в массиве «обещаний» есть хотя бы одно, которое «отклонено» (rejected), то метод Promise.all так же вернет «обещание» с таким состоянием, не дожидаясь прохождения по всему массиву.
Поэтому напишем функцию loadAndDisplayImages.
Подгружаем изображения, и показываем их на странице все сразу
function loadAndDisplayImages
/**
*
* @param imgList - массив url
* @returns {Promise}
*/
function loadAndDisplayImages(imgList)
{
var notLoaded = [];//сохраним url, какие не были загружены
var loaded = [];//сохраним url, какие были загружены
var promiseImgs = imgList.map(loadImage);
//вернем результат работы вызова reduce(...) - объект Promise, чтобы можно было потом при необходимости продолжить цепочку вызовов:
//loadAndDisplayImages(...).then(...).catch(...);
return promiseImgs.reduce(function (previousPromise, currentPromise)
{
return previousPromise
.then(function()
{
//выполняется этот участок кода, так как previousPromise - в состоянии resolved (= Promise.resolve())
return currentPromise;
})
.then(function(url) //для "обещаний" в состоянии resolved
{
$('#images').append('<img src="'+url+'" style="width: 200px;"/>');
loaded.push(url);
return Promise.resolve(url);
})
.catch(function(url)//для "обещаний" в состоянии rejected
{
console.log('не удалось загрузить изображение по указанному пути: ', url);
notLoaded.push(url);
return Promise.resolve(url);
});
}, Promise.resolve())
.then(function (lastUrl)
{
console.log('lastUrl:', lastUrl);
let res = {loaded: loaded, notLoaded: notLoaded};
//но мы вернем Promise, значение которого будет объект
return Promise.resolve(res);
});
}
loadAndDisplayImages(imgList)
.then(function(loadRes)
{
console.log(loadRes);
})
.catch(function(err)
{
console.log(err);
});
Можно посмотреть сетевую активность в браузере и убедиться в параллельной работе (для наглядности в Chrome была включена эмуляция подключения по Wi-Fi (2ms, 30Mb/s, 15M/s):
Разобравшись, как работать с Promise, вам будет проще понять принципы работы, например, с API Яндекс.Карт, или Service Worker – именно там они используются.
UPD: В статье не озвучил один важный момент, с которым, отчасти, был связан совет писать return resolve() или return reject().
Когда вызываются данные методы, «обещание» устанавливается в свое конечное состояние «выполнено» или «отклонено», соответственно. После этого состояние изменить нельзя. Примеры можно посмотреть в комментарии.