Объекты Promise упрощают отложенные и асинхронные вычисления. Объект Promise представляет операцию, которая ещё не завершена.
Разработчики, приготовьтесь к поворотному моменту в истории веб-разработки.
[Барабанная дробь]
В JavaScript появились промисы.
[Взрываются фейерверки, сверху сыплются блестки, толпа ликует]
На этом этапе вы попадаете в одну из следующих категорий:
- Вокруг вас ликуют люди, но вы не понимаете, в чем дело. Возможно, вы даже не знаете, что такое "обещание". Вы бы пожали плечами, но тяжесть блестящей бумаги давит на ваши плечи. Если да, не волнуйтесь, мне потребовалось много времени, чтобы понять, почему меня это должно волновать. Рекомендуем начать с первого шага.
- Вы бьете воздух! Наконец-то, правда? Вы уже использовали промисы, но вас беспокоит, что у всех реализаций немного разные API. Какой API используется для официальной версии JavaScript? Начать лучше всего с терминологии.
- Вы уже знали об этом и смеетесь над теми, кто прыгает от радости, как будто это для них новость. Порадуйтесь своему успеху и переходите к документации по API.
Поддержка браузеров и полифил
Чтобы обеспечить соответствие спецификации в браузерах, в которых не полностью реализованы обещания, или добавить обещания в другие браузеры и Node.js, ознакомьтесь с полифилом (2 КБ в сжатом виде).
Из-за чего столько шума?
JavaScript – однопоточный язык, то есть два фрагмента скрипта не могут выполняться одновременно, а только по очереди. В браузерах JavaScript делит поток с множеством других вещей, которые различаются от браузера к браузеру. Но обычно JavaScript находится в той же очереди, что и отрисовка, обновление стилей и обработка действий пользователя (например, выделение текста и взаимодействие с элементами управления формы). Активность в одном из них задерживает другие.
Как человек, вы многопоточны. Вы можете печатать несколькими пальцами, вы можете вести машину и одновременно разговаривать. Единственная блокирующая функция, с которой нам приходится иметь дело, – это чихание, когда вся текущая деятельность должна быть приостановлена на время чихания. Это довольно раздражает, особенно когда вы за рулем и пытаетесь вести разговор. Вы же не хотите писать код, который чихает.
Вероятно, вы использовали события и обратные вызовы, чтобы обойти это ограничение. Вот мероприятия:
var img1 = document.querySelector('.img-1');
img1.addEventListener('load', function() {
// woo yey image loaded
});
img1.addEventListener('error', function() {
// argh everything's broken
});
Это совсем не чихание. Мы получаем изображение, добавляем пару слушателей, а затем JavaScript может прекратить выполнение, пока не будет вызван один из этих слушателей.
К сожалению, в приведенном выше примере события могли произойти до того, как мы начали их прослушивать, поэтому нам нужно обойти это, используя свойство "complete" изображений:
var img1 = document.querySelector('.img-1');
function loaded() {
// woo yey image loaded
}
if (img1.complete) {
loaded();
}
else {
img1.addEventListener('load', loaded);
}
img1.addEventListener('error', function() {
// argh everything's broken
});
Это не позволяет отслеживать изображения, которые не загрузились до того, как мы начали их прослушивать. К сожалению, DOM не позволяет нам это сделать. Кроме того, это загрузка одного изображения. Все становится ещё сложнее, если мы хотим знать, когда загрузился набор изображений.
События не всегда являются лучшим способом
События отлично подходят для действий, которые могут происходить несколько раз с одним и тем же объектом, например keyup, touchstart и т. д. При этом не имеет значения, что происходило до подключения прослушивателя. Но когда дело доходит до
асинхронного успеха/неудачи, в идеале вы хотите что-то вроде этого:
img1.callThisIfLoadedOrWhenLoaded(function() {
// loaded
}).orIfFailedCallThis(function() {
// failed
});
// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
// all loaded
}).orIfSomeFailedCallThis(function() {
// one or more failed
});
Это то, что делают обещания, но с лучшим именованием. Если бы у элементов изображений HTML был метод ready, возвращающий объект Promise, мы могли бы сделать следующее:
img1.ready()
.then(function() {
// loaded
}, function() {
// failed
});
// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
// all loaded
}, function() {
// one or more failed
});
В целом обещания похожи на прослушиватели событий, но есть два отличия:
- Обещание может быть выполнено или не выполнено только один раз. Оно не может быть выполнено или не выполнено дважды, а также не может перейти из состояния выполнения в состояние невыполнения и наоборот.
- Если обещание выполнено или не выполнено, а вы позже добавите обратный вызов, он будет выполнен, даже если событие произошло раньше.
Это очень полезно для асинхронного успеха/неудачи, потому что вы меньше заинтересованы в точном времени, когда что-то стало доступно, и больше заинтересованы в реагировании на результат.
Терминология, связанная с обещаниями
Доменик Деникола (Domenic Denicola) вычитал первый черновик этой статьи и поставил мне двойку за терминологию. Он оставил меня после уроков, заставил 100 раз переписать Государства и судьбы и отправил моим родителям письмо, в котором выразил свою обеспокоенность. Несмотря на это, я всё ещё путаю многие термины, но вот основные:
Обещание может быть:
- fulfilled – действие, связанное с обещанием, выполнено успешно.
- rejected – действие, связанное с обещанием, не выполнено;
- pending – запрос ещё не выполнен и не отклонен.
- settled – выполнено или отклонено.
В спецификации также используется термин thenable для описания объекта, похожего на Promise, поскольку у него есть метод then. Этот термин напоминает мне о бывшем тренере сборной Англии по футболу Терри Венейблсе, поэтому я буду использовать его как можно реже.
Обещания в JavaScript
Обещания уже давно существуют в виде библиотек, таких как:
Описанные выше промисы и промисы JavaScript имеют общее стандартизированное поведение, называемое Promises/A+. Если вы используете jQuery, то в этой библиотеке есть похожий механизм, который называется Deferreds. Однако Deferreds не соответствуют спецификации Promise/A+, поэтому они немного отличаются и менее полезны. Будьте внимательны. В jQuery также есть тип Promise, но это лишь подмножество Deferred, и у него те же проблемы.
Хотя реализации промисов следуют стандартизированному поведению, их общие API различаются. Объекты Promise в JavaScript похожи на API RSVP.js. Вот как создать обещание:
var promise = new Promise(function(resolve, reject) {
// do a thing, possibly async, then…
if (/* everything turned out fine */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
});
Конструктор Promise принимает один аргумент – обратный вызов с двумя параметрами: resolve и reject. Выполните действие в обратном вызове, возможно, асинхронно, а затем вызовите resolve, если все сработало, или reject в противном случае.
Как и в случае с throw в обычном JavaScript, принято, но не обязательно, отклонять с объектом Error. Преимущество объектов Error заключается в том, что они фиксируют трассировку стека, что делает инструменты отладки более полезными.
Вот как это работает:
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
then() принимает два аргумента: обратный вызов для успешного распознавания и обратный вызов для отклонения. Оба обратных вызова необязательны, поэтому вы можете добавить только один из них.
Объекты Promise в JavaScript изначально появились в DOM как Futures, затем были переименованы в Promises и в итоге перенесены в JavaScript. То, что они находятся в JavaScript, а не в DOM, очень удобно, поскольку они будут доступны в контекстах JS, не связанных с браузером, таких как Node.js (используют ли они их в своих основных API – это другой вопрос).
Хотя это функция JavaScript, DOM не боится их использовать. Фактически, все новые DOM API с асинхронными методами успеха/неудачи будут использовать промисы. Это уже происходит с Quota Management, Font Load Events, ServiceWorker, Web MIDI, Streams и другими API.
Совместимость с другими библиотеками
API промисов JavaScript будет обрабатывать любой объект с методом then() как промис (или thenable на языке промисов sigh). Поэтому если вы используете библиотеку, которая возвращает промис Q, то он будет корректно работать с новыми промисами JavaScript.
Хотя, как я уже говорил, Deferreds в jQuery не очень удобны. К счастью, их можно преобразовать в стандартные промисы, что и рекомендуется сделать как можно скорее:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
В этом примере метод $.ajax библиотеки jQuery возвращает объект Deferred. Поскольку у него есть метод then(), Promise.resolve() может преобразовать его в объект Promise из JavaScript. Однако иногда отложенные объекты передают в обратные вызовы несколько аргументов, например:
var jqDeferred = $.ajax('/whatever.json');
jqDeferred.then(function(response, statusText, xhrObj) {
// ...
}, function(xhrObj, textStatus, err) {
// ...
})
а в JS – только первый:
jsPromise.then(function(response) {
// ...
}, function(xhrObj) {
// ...
})
К счастью, обычно это то, что вам нужно, или, по крайней мере, дает вам доступ к тому, что вам нужно. Также имейте в виду, что jQuery не следует соглашению о передаче объектов Error в отклонения.
Упрощение сложного асинхронного кода
Хорошо, давайте напишем код. Допустим, вы хотите:
- Запустите индикатор загрузки.
- Получить JSON-файл для истории, в котором есть название и URL каждой главы.
- Добавить заголовок на страницу
- Получить каждый раздел
- Добавьте историю на страницу
- Останови спиннер
…но и сообщать пользователю, если что-то пошло не так. В этот момент мы также захотим остановить спиннер, иначе он продолжит вращаться, у него закружится голова, и он врежется в какой-нибудь другой пользовательский интерфейс.
Конечно, вы не будете использовать JavaScript для доставки истории, поскольку HTML быстрее, но этот шаблон довольно распространен при работе с API: несколько выборок данных, а затем выполнение действий, когда все будет готово.
Для начала давайте разберемся с получением данных из сети:
Использование объектов Promise в XMLHttpRequest
Старые API будут обновлены для использования промисов, если это возможно с сохранением обратной совместимости. XMLHttpRequest – отличный кандидат, но пока давайте напишем простую функцию для выполнения GET-запроса:
function get(url) {
// Return a new promise.
return new Promise(function(resolve, reject) {
// Do the usual XHR stuff
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
// This is called even on 404 etc
// so check the status
if (req.status == 200) {
// Resolve the promise with the response text
resolve(req.response);
}
else {
// Otherwise reject with the status text
// which will hopefully be a meaningful error
reject(Error(req.statusText));
}
};
// Handle network errors
req.onerror = function() {
reject(Error("Network Error"));
};
// Make the request
req.send();
});
}
Теперь давайте воспользуемся им:
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.error("Failed!", error);
})
Теперь мы можем делать HTTP-запросы, не вводя вручную XMLHttpRequest. Это здорово, потому что чем меньше я вижу раздражающий верблюжий регистр XMLHttpRequest, тем счастливее моя жизнь.
Цепочки
then() – это не конец истории. Вы можете объединять then в цепочки, чтобы преобразовывать значения или выполнять дополнительные асинхронные действия одно за другим.
Преобразование значений
Вы можете преобразовать значения, просто возвращая новое значение:
var promise = new Promise(function(resolve, reject) {
resolve(1);
});
promise.then(function(val) {
console.log(val); // 1
return val + 2;
}).then(function(val) {
console.log(val); // 3
})
В качестве практического примера вернемся к:
get('story.json').then(function(response) {
console.log("Success!", response);
})
Ответ должен быть в формате JSON, но сейчас мы получаем его в виде обычного текста. Мы могли бы изменить функцию get, чтобы использовать JSON responseType, но мы также можем решить эту проблему с помощью промисов:
get('story.json').then(function(response) {
return JSON.parse(response);
}).then(function(response) {
console.log("Yey JSON!", response);
})
Поскольку JSON.parse() принимает один аргумент и возвращает преобразованное значение, мы можем использовать сокращенную запись:
get('story.json').then(JSON.parse).then(function(response) {
console.log("Yey JSON!", response);
})
На самом деле, мы можем очень легко создать функцию getJSON():
function getJSON(url) {
return get(url).then(JSON.parse);
}
getJSON() по-прежнему возвращает обещание, которое извлекает URL-адрес, а затем анализирует ответ в формате JSON.
Постановка асинхронных действий в очередь
Вы также можете объединять then в цепочки, чтобы выполнять асинхронные действия последовательно.
Когда вы возвращаете что-то из обратного вызова then(), происходит небольшое волшебство.
Если вы возвращаете значение, следующая функция then() вызывается с этим значением. Однако если вы возвращаете что-то похожее на промис, следующий then() ожидает его и вызывается только тогда, когда этот промис урегулирован (успешно/неудачно). Пример:
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
console.log("Got chapter 1!", chapter1);
})
Здесь мы отправляем асинхронный запрос к story.json, который возвращает набор URL для запроса, а затем запрашиваем первый из них. Вот когда обещания
действительно начинают выделяться из простых шаблонов обратного вызова.
Вы даже можете создать быстрый способ получения эпизодов:
var storyPromise;
function getChapter(i) {
storyPromise = storyPromise || getJSON('story.json');
return storyPromise.then(function(story) {
return getJSON(story.chapterUrls[i]);
})
}
// and using it is simple:
getChapter(0).then(function(chapter) {
console.log(chapter);
return getChapter(1);
}).then(function(chapter) {
console.log(chapter);
})
Мы не скачиваем story.json, пока не будет вызван getChapter, но при последующих вызовах getChapter мы повторно используем обещание истории, поэтому story.json извлекается только один раз. Объекты Promise
Обработка ошибок
Как мы уже видели, then() принимает два аргумента: один для успешного выполнения, другой для ошибки (или, на языке объектов Promise, для выполнения и отклонения):
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.log("Failed!", error);
})
Вы также можете использовать catch():
get('story.json').then(function(response) {
console.log("Success!", response);
}).catch(function(error) {
console.log("Failed!", error);
})
В catch() нет ничего особенного, это просто синтаксический сахар для then(undefined, func), но он более читабелен. Обратите внимание, что два приведенных выше примера кода ведут себя по-разному. Последний эквивалентен следующему:
get('story.json').then(function(response) {
console.log("Success!", response);
}).then(undefined, function(error) {
console.log("Failed!", error);
})
Разница небольшая, но очень полезная. Отклонения Promise переходят к следующему методу then() с обратным вызовом отклонения (или catch(), поскольку это эквивалентно). При использовании then(func1, func2) будет вызываться либо func1, либо func2, но не оба метода. Но с then(func1).catch(func2) будут вызваны оба, если func1 отклонит, так как это отдельные шаги в цепочке. Принимайте
следующие препараты:
asyncThing1().then(function() {
return asyncThing2();
}).then(function() {
return asyncThing3();
}).catch(function(err) {
return asyncRecovery1();
}).then(function() {
return asyncThing4();
}, function(err) {
return asyncRecovery2();
}).catch(function(err) {
console.log("Don't worry about it");
}).then(function() {
console.log("All done!");
})
Приведенный выше код очень похож на обычную конструкцию try/catch в JavaScript. Ошибки, возникающие в блоке try, сразу же переходят в блок catch(). Вот как это выглядит в виде блок-схемы (я их обожаю):
Синие линии показывают выполненные обещания, а красные – отклоненные.
Исключения и обещания JavaScript
Отклонения происходят, когда объект Promise явно отклоняется, а также неявно, если в обратном вызове конструктора возникает ошибка:
var jsonPromise = new Promise(function(resolve, reject) {
// JSON.parse throws an error if you feed it some
// invalid JSON, so this implicitly rejects:
resolve(JSON.parse("This ain't JSON"));
});
jsonPromise.then(function(data) {
// This never happens:
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
Это означает, что все действия, связанные с обещанием, полезно выполнять внутри обратного вызова конструктора обещания, чтобы ошибки автоматически перехватывались и становились отклонениями.
То же самое относится к ошибкам, возникающим в обратных вызовах then().
get('/').then(JSON.parse).then(function() {
// This never happens, '/' is an HTML page, not JSON
// so JSON.parse throws
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
Обработка ошибок на практике
В нашем примере с рассказом и главами мы можем использовать catch, чтобы показать пользователю сообщение об ошибке:
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
addHtmlToPage(chapter1.html);
}).catch(function() {
addTextToPage("Failed to show chapter");
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
Если получить story.chapterUrls[0] не удается (например, из-за ошибки HTTP 500 или отсутствия подключения к интернету), все последующие обратные вызовы, включая тот, который находится в getJSON() и пытается обработать ответ как JSON, будут пропущены. Также будет пропущен обратный вызов, который добавляет на страницу chapter1.html. Вместо этого он переходит к обратному вызову catch. В результате на страницу будет добавлено сообщение "Не удалось показать главу", если какое-либо из предыдущих действий не удалось выполнить.
Как и в случае с try/catch в JavaScript, ошибка перехватывается, и последующий код продолжает выполняться, поэтому индикатор загрузки всегда скрыт, что нам и нужно. Приведенный выше код становится неблокирующей асинхронной версией следующего кода:
try {
var story = getJSONSync('story.json');
var chapter1 = getJSONSync(story.chapterUrls[0]);
addHtmlToPage(chapter1.html);
}
catch (e) {
addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'
Вы можете использовать catch() просто для ведения журнала, не исправляя ошибку. Для этого просто повторно сгенерируйте ошибку. Это можно сделать в методе getJSON():
function getJSON(url) {
return get(url).then(JSON.parse).catch(function(err) {
console.log("getJSON failed for", url, err);
throw err;
});
}
Итак, мы получили одну главу, но нам нужны все. Давайте сделаем это.
Параллелизм и последовательность: как получить лучшее от обоих подходов
Думать асинхронно непросто. Если вам трудно начать, попробуйте написать код так, как если бы он был синхронным. В этом случае:
try {
var story = getJSONSync('story.json');
addHtmlToPage(story.heading);
story.chapterUrls.forEach(function(chapterUrl) {
var chapter = getJSONSync(chapterUrl);
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}
catch (err) {
addTextToPage("Argh, broken: " + err.message);
}
document.querySelector('.spinner').style.display = 'none'
Отлично! Однако он синхронный и блокирует браузер во время загрузки. Чтобы сделать этот процесс асинхронным, мы используем then(), чтобы действия выполнялись одно за другим.
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
Но как мы можем перебрать URL-адреса глав и получить их по порядку? Неправильно:
story.chapterUrls.forEach(function(chapterUrl) {
// Fetch chapter
getJSON(chapterUrl).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
})
forEach не поддерживает асинхронность, поэтому главы будут появляться в том порядке, в котором они загружаются. Примерно так же было написано "Криминальное чтиво". Это не "Криминальное чтиво", так что давайте исправим это.
Как создать серийный показ
Мы хотим превратить наш массив chapterUrls в последовательность обещаний. Это можно сделать с помощью then():
// Start off with a promise that always resolves
var sequence = Promise.resolve();
// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
// Add these actions to the end of the sequence
sequence = sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
})
Здесь мы впервые видим Promise.resolve(), который создает обещание, разрешающееся в любое значение, которое вы ему передаете. Если передать ему экземпляр Promise, он просто вернет его (примечание: это изменение в спецификации, которому некоторые реализации пока не следуют). Если передать ему что-то похожее на обещание (имеет метод then()), он создаст подлинный объект Promise, который выполняется или отклоняется таким же образом. Если вы передадите любое другое значение, например Promise.resolve('Hello'), будет создан промис, который выполнится с этим значением. Если вы вызовете его без значения, как показано выше, он выполнится с "undefined".
Также есть метод Promise.reject(val), который создает объект Promise, отклоняемый с заданным значением (или undefined).
Приведенный выше код можно упростить с помощью array.reduce:
// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Add these actions to the end of the sequence
return sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve())
Этот пример аналогичен предыдущему, но в нем не используется отдельная переменная sequence. Функция обратного вызова reduce вызывается для каждого элемента массива.
При первом вызове значение sequence равно Promise.resolve(), а при последующих – тому, что было возвращено в предыдущем вызове. array.reduce
очень полезен для сведения массива к одному значению, которое в данном случае является обещанием.
Подведем итоги:
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
return story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Once the last chapter's promise is done…
return sequence.then(function() {
// …fetch the next chapter
return getJSON(chapterUrl);
}).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
Вот и всё. Мы получили полностью асинхронную версию синхронного кода. Но мы можем сделать лучше. Сейчас наша страница загружается так:
Браузеры довольно хорошо скачивают несколько вещей одновременно, поэтому мы теряем производительность, скачивая главы одну за другой. Мы хотим скачать их все одновременно, а затем обработать, когда они все прибудут. К счастью, для этого есть API:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
})
Promise.all принимает массив обещаний и создает обещание, которое выполняется, когда все они успешно завершаются. Вы получите массив результатов (все, что было выполнено) в том же порядке, что и переданные обещания.
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// Take an array of promises and wait on them all
return Promise.all(
// Map our array of chapter urls to
// an array of chapter json promises
story.chapterUrls.map(getJSON)
);
}).then(function(chapters) {
// Now we have the chapters jsons in order! Loop through…
chapters.forEach(function(chapter) {
// …and add to the page
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened so far
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
В зависимости от подключения это может быть на несколько секунд быстрее, чем загрузка по одному, и это меньше кода, чем в нашей первой попытке. Главы могут скачиваться в любом порядке, но на экране они появляются в правильном порядке.
Однако мы все равно можем улучшить воспринимаемую производительность. Когда придет первая глава, мы добавим ее на страницу. Это позволяет пользователю начать чтение до того, как будут получены остальные главы. Когда появится третья глава, мы не добавим ее на страницу, потому что пользователь может не понять, что вторая глава отсутствует. Когда выйдет вторая глава, мы добавим вторую и третью главы и т. д.
Для этого мы одновременно получаем JSON для всех глав, а затем создаем последовательность, чтобы добавить их в документ:
getJSON('story.json')
.then(function(story) {
addHtmlToPage(story.heading);
// Map our array of chapter urls to
// an array of chapter json promises.
// This makes sure they all download in parallel.
return story.chapterUrls.map(getJSON)
.reduce(function(sequence, chapterPromise) {
// Use reduce to chain the promises together,
// adding content to the page for each chapter
return sequence
.then(function() {
// Wait for everything in the sequence so far,
// then wait for this chapter to arrive.
return chapterPromise;
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
Вот и все. Лучшее из двух миров! Для доставки всего контента требуется столько же времени, но пользователь получает первую часть контента раньше.
В этом простом примере все главы загружаются примерно в одно и то же время, но преимущество отображения по одной главе будет более заметным при наличии большего количества более крупных глав.
Если бы мы использовали обратные вызовы или события в стиле Node.js, кода было бы вдвое больше, а главное – он был бы менее понятным. Однако это не конец истории с промисами. В сочетании с другими функциями ES6 они становятся ещё проще.
Дополнительный раунд: расширенные возможности
С тех пор как я написал эту статью, возможности использования промисов значительно расширились. Начиная с версии Chrome 55, асинхронные функции позволяют писать код на основе промисов так, как если бы он был синхронным, но без блокировки основного потока. Подробнее об этом можно прочитать в моей статье об асинхронных функциях. Обе эти функции широко поддерживаются в основных браузерах. Подробную информацию можно найти в справочных материалах MDN по Promise и асинхронным функциям.
Большое спасибо Анне ван Кестерен (Anne van Kesteren), Доменику Дениколе (Domenic Denicola), Тому Эшворту (Tom Ashworth), Реми Шарпу (Remy Sharp), Эдди Османи (Addy Osmani), Артуру Эвансу (Arthur Evans) и Ютаке Хирано (Yutaka Hirano), которые вычитали этот текст и внесли исправления и рекомендации.
Также благодарим Матиаса Байненса (Mathias Bynens) за обновление различных разделов статьи.