Вернемся к проблеме, упомянутой в главе «Введение»: обратные вызовы: у нас есть последовательность асинхронных задач, которые необходимо выполнить одна за другой — например, загрузка скриптов. Как мы можем это хорошо закодировать?
Обещания предоставляют несколько рецептов, как это сделать.
В этой главе мы рассмотрим цепочку обещаний.
Это выглядит так:
новое обещание (функция (разрешить, отклонить) { setTimeout(() => разрешить(1), 1000); // (*) }).then(function(result) { // (**) оповещение (результат); // 1 вернуть результат * 2; }).then(function(result) { // (***) оповещение (результат); // 2 вернуть результат * 2; }).then(функция(результат) { оповещение (результат); // 4 вернуть результат * 2; });
Идея состоит в том, что результат передается через цепочку обработчиков .then
.
Вот поток:
Первоначальное обещание выполняется за 1 секунду (*)
,
Затем вызывается обработчик .then
(**)
, который, в свою очередь, создает новое обещание (разрешается со значением 2
).
Следующий then
(***)
получает результат предыдущего, обрабатывает его (удваивает) и передает следующему обработчику.
…и так далее.
Когда результат передается по цепочке обработчиков, мы видим последовательность вызовов alert
: 1
→ 2
→ 4
.
Все это работает, потому что каждый вызов .then
возвращает новое обещание, так что мы можем вызвать для него следующий .then
.
Когда обработчик возвращает значение, оно становится результатом этого обещания, поэтому с ним вызывается следующий .then
.
Классическая ошибка новичков: технически мы также можем добавить множество .then
к одному обещанию. Это не цепочка.
Например:
пусть обещание = новое обещание (функция (разрешить, отклонить) { setTimeout(() => разрешить(1), 1000); }); обещание.тогда(функция(результат) { оповещение (результат); // 1 вернуть результат * 2; }); обещание.тогда(функция(результат) { оповещение (результат); // 1 вернуть результат * 2; }); обещание.тогда(функция(результат) { оповещение (результат); // 1 вернуть результат * 2; });
Здесь мы просто добавили несколько обработчиков к одному обещанию. Они не передают результат друг другу; вместо этого они обрабатывают его независимо.
Вот изображение (сравните его с цепочкой выше):
Все .then
на одном и том же обещании получают один и тот же результат – результат этого обещания. Итак, в приведенном выше коде все alert
показывают одно и то же: 1
.
На практике нам редко требуется несколько обработчиков для одного обещания. Цепочка используется гораздо чаще.
Обработчик, используемый в .then(handler)
может создавать и возвращать обещание.
В этом случае дальнейшие обработчики ждут, пока оно стабилизируется, а затем получают результат.
Например:
новое обещание (функция (разрешить, отклонить) { setTimeout(() => разрешить(1), 1000); }).then(функция(результат) { оповещение (результат); // 1 return new Promise((разрешить, отклонить) => { // (*) setTimeout(() => разрешить(результат * 2), 1000); }); }).then(function(result) { // (**) оповещение (результат); // 2 вернуть новое обещание((разрешить, отклонить) => { setTimeout(() => разрешить(результат * 2), 1000); }); }).then(функция(результат) { оповещение (результат); // 4 });
Здесь первый .then
показывает 1
и возвращает new Promise(…)
в строке (*)
. Через одну секунду он разрешается, и результат (аргумент resolve
, здесь result * 2
) передается обработчику второго .then
. Этот обработчик находится в строке (**)
, он показывает 2
и делает то же самое.
Таким образом, вывод такой же, как и в предыдущем примере: 1 → 2 → 4, но теперь с задержкой в 1 секунду между вызовами alert
.
Возврат промисов позволяет нам выстраивать цепочки асинхронных действий.
Давайте воспользуемся этой функцией вместе с обещанным loadScript
, определенным в предыдущей главе, для последовательной загрузки сценариев один за другим:
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(функция(скрипт) { return loadScript("https://javascript.info/article/promise-chaining/two.js"); }) .then(функция(скрипт) { return loadScript("https://javascript.info/article/promise-chaining/three.js"); }) .then(функция(скрипт) { // используем функции, объявленные в скриптах // чтобы показать, что они действительно загрузились один(); два(); три(); });
Этот код можно сделать немного короче с помощью стрелочных функций:
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(script => loadScript("https://javascript.info/article/promise-chaining/two.js")) .then(script => loadScript("https://javascript.info/article/promise-chaining/three.js")) .then(скрипт => { // скрипты загружены, мы можем использовать объявленные там функции один(); два(); три(); });
Здесь каждый вызов loadScript
возвращает обещание, а следующий .then
запускается, когда оно разрешается. Затем он инициирует загрузку следующего скрипта. Таким образом, скрипты загружаются один за другим.
Мы можем добавить в цепочку больше асинхронных действий. Обратите внимание, что код по-прежнему «плоский» — он растет вниз, а не вправо. Никаких признаков «пирамиды гибели» нет.
Технически мы могли бы добавить .then
непосредственно к каждому loadScript
, вот так:
loadScript("https://javascript.info/article/promise-chaining/one.js").then(script1 => { loadScript("https://javascript.info/article/promise-chaining/two.js").then(script2 => { loadScript("https://javascript.info/article/promise-chaining/three.js").then(script3 => { // эта функция имеет доступ к переменным script1, script2 и script3 один(); два(); три(); }); }); });
Этот код делает то же самое: последовательно загружает 3 скрипта. Но оно «растет вправо». Итак, у нас та же проблема, что и с обратными вызовами.
Люди, которые начинают использовать промисы, иногда не знают о цепочках, поэтому пишут так. Как правило, цепочка является предпочтительной.
Иногда можно написать .then
напрямую, потому что вложенная функция имеет доступ к внешней области. В приведенном выше примере наиболее вложенный обратный вызов имеет доступ ко всем переменным script1
, script2
, script3
. Но это скорее исключение, чем правило.
Тенаблс
Точнее, обработчик может возвращать не совсем промис, а так называемый «thenable» объект — произвольный объект, имеющий метод .then
. Это будет рассматриваться так же, как обещание.
Идея состоит в том, что сторонние библиотеки могут реализовывать собственные «совместимые с обещаниями» объекты. Они могут иметь расширенный набор методов, но при этом быть совместимыми с собственными промисами, поскольку реализуют .then
.
Вот пример объекта thenable:
класс Тогдаабл { конструктор(число) { this.num = число; } then(решить, отклонить) { оповещение (решить); // функция() { собственный код } // разрешаем это.num*2 через 1 секунду setTimeout(() =>solve(this.num * 2), 1000); // (**) } } новое обещание (решить => решить (1)) .then(результат => { вернуть новый thenable(результат); // (*) }) .then(предупреждение); // показывает 2 через 1000 мс
JavaScript проверяет объект, возвращаемый обработчиком .then
, в строке (*)
: если у него есть вызываемый метод с именем then
, он вызывает этот метод, предоставляя собственные функции, resolve
, reject
в качестве аргументов (аналогично исполнителю) и ждет, пока один из них называется. В приведенном выше resolve(2)
вызывается через 1 секунду (**)
. Затем результат передается дальше по цепочке.
Эта функция позволяет нам интегрировать пользовательские объекты с цепочками обещаний без необходимости наследования от Promise
.
Во внешнем программировании промисы часто используются для сетевых запросов. Итак, давайте посмотрим расширенный пример этого.
Мы будем использовать метод fetch для загрузки информации о пользователе с удаленного сервера. Он имеет множество необязательных параметров, описанных в отдельных главах, но основной синтаксис довольно прост:
пусть обещание = выборка (URL);
Это делает сетевой запрос к url
и возвращает обещание. Промис разрешается с помощью объекта response
, когда удаленный сервер отвечает заголовками, но до загрузки полного ответа .
Чтобы прочитать полный ответ, мы должны вызвать метод response.text()
: он возвращает обещание, которое разрешается, когда полный текст загружается с удаленного сервера, и в результате получается этот текст.
Код ниже отправляет запрос к user.json
и загружает его текст с сервера:
fetch('https://javascript.info/article/promise-chaining/user.json') // .then ниже запускается, когда удаленный сервер отвечает .then(функция(ответ) { // response.text() возвращает новое обещание, которое разрешается с полным текстом ответа // когда он загружается вернуть ответ.текст(); }) .then(функция(текст) { // ...а вот содержимое удаленного файла предупреждение (текст); // {"name": "iliakan", "isAdmin": true} });
Объект response
, возвращаемый функцией fetch
также включает метод response.json()
, который считывает удаленные данные и анализирует их как JSON. В нашем случае это даже удобнее, поэтому перейдем к нему.
Для краткости мы также будем использовать стрелочные функции:
// то же, что и выше, но response.json() анализирует удаленный контент как JSON fetch('https://javascript.info/article/promise-chaining/user.json') .then(ответ => ответ.json()) .then(user => alert(user.name)); // iliakan, получили имя пользователя
Теперь давайте что-нибудь сделаем с загруженным пользователем.
Например, мы можем сделать еще один запрос к GitHub, загрузить профиль пользователя и показать аватар:
// Делаем запрос для user.json fetch('https://javascript.info/article/promise-chaining/user.json') // Загрузите его как JSON .then(ответ => ответ.json()) // Делаем запрос на GitHub .then(user => fetch(`https://api.github.com/users/${user.name}`)) // Загружаем ответ в формате JSON .then(ответ => ответ.json()) // Показываем изображение аватара (githubUser.avatar_url) на 3 секунды (возможно, анимируем его) .then(githubUser => { пусть img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "обещание-аватар-пример"; document.body.append(img); setTimeout(() => img.remove(), 3000); // (*) });
Код работает; подробности см. в комментариях. Однако в этом есть потенциальная проблема, типичная ошибка для тех, кто начинает использовать промисы.
Посмотрите на строку (*)
: как мы можем что-то сделать после того, как аватар закончил показываться и был удален? Например, мы хотели бы показать форму для редактирования этого пользователя или что-то еще. На данный момент нет никакой возможности.
Чтобы сделать цепочку расширяемой, нам нужно вернуть обещание, которое выполняется, когда аватар завершает показ.
Так:
fetch('https://javascript.info/article/promise-chaining/user.json') .then(ответ => ответ.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(ответ => ответ.json()) .then(githubUser => новое обещание(функция(разрешить, отклонить) { // (*) пусть img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "обещание-аватар-пример"; document.body.append(img); setTimeout(() => { img.remove(); разрешить (githubUser); // (**) }, 3000); })) // срабатывает через 3 секунды .then(githubUser => alert(`Отображение ${githubUser.name}` закончено`));
То есть обработчик .then
в строке (*)
теперь возвращает new Promise
, который становится выполненным только после resolve(githubUser)
в setTimeout
(**)
. Следующий .then
в цепочке будет ждать этого.
Рекомендуется асинхронное действие всегда возвращать обещание. Это дает возможность планировать действия после него; даже если мы не планируем расширять цепочку сейчас, это может понадобиться нам позже.
Наконец, мы можем разделить код на повторно используемые функции:
функция loadJson (url) { обратная выборка (url) .then(ответ => ответ.json()); } функция loadGithubUser (имя) { return loadJson(`https://api.github.com/users/${name}`); } функция showAvatar(githubUser) { вернуть новое обещание (функция (разрешить, отклонить) { пусть img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "обещание-аватар-пример"; document.body.append(img); setTimeout(() => { img.remove(); разрешить (githubUser); }, 3000); }); } // Используем их: loadJson('https://javascript.info/article/promise-chaining/user.json') .then(user => loadGithubUser(user.name)) .then(показать аватар) .then(githubUser => alert(`Отображение ${githubUser.name}` закончено`)); // ...
Если обработчик .then
(или catch/finally
, не имеет значения) возвращает обещание, остальная часть цепочки ждет, пока оно не стабилизируется. Когда это происходит, его результат (или ошибка) передается дальше.
Вот полная картина:
Эти фрагменты кода равны? Другими словами, ведут ли они себя одинаково при любых обстоятельствах и для любых функций-обработчиков?
обещание.тогда(f1).catch(f2);
Против:
обещание.тогда(f1, f2);
Короткий ответ: нет, они не равны :
Разница в том, что если ошибка возникает в f1
, то здесь она обрабатывается .catch
:
обещать .тогда(f1) .catch(f2);
…Но не здесь:
обещать .then(f1, f2);
Это потому, что ошибка передается по цепочке, а во втором фрагменте кода нет цепочки ниже f1
.
Другими словами, .then
передает результаты/ошибки следующему .then/catch
. Итак, в первом примере ниже есть catch
, а во втором его нет, поэтому ошибка не обрабатывается.