Then js что это
Ранее мы рассмотрели, как из функции промиса мы можем передать во вне результат асинхронной операции:
const myPromise = new Promise(function(resolve)< console.log("Выполнение асинхронной операции"); resolve("Привет мир!"); >);
Теперь получим это значение. Для получения результата операции промиса применяется функция then() объекта Promise :
then(onFulfilled, onRejected);
Первый параметр функции — onFulfilled представляет функцию, которая выполняется при успешном завершении промиса и в качестве параметра получает переданные в resolve() данные.
Второй параметр функции — onRejected представляет функцию, которая выполняется при возникновении ошибки и в качестве параметра получает переданные в reject() данные.
Функция then() возвращает также объект Promise .
Так, получим переданные данные:
const myPromise = new Promise(function(resolve)< console.log("Выполнение асинхронной операции"); resolve("Привет мир!"); >); myPromise.then(function(value)< console.log(`Из промиса получены данные: $`); >)
То есть параметр value здесь будет представлять строку «Привет мир!» , которая передается в resolve(«Привет мир!») . В итоге консольный вывод будет выглядеть следующим образом:
Выполнение асинхронной операции Из промиса получены данные: Привет мир!
Для примера вызове несколько промисов, чтобы увидеть асинхронность в деле:
const myPromise3000 = new Promise(function(resolve)< console.log("[myPromise3000] Выполнение асинхронной операции"); setTimeout(()=>, 3000); >); const myPromise1000 = new Promise(function(resolve)< console.log("[myPromise1000] Выполнение асинхронной операции"); setTimeout(()=>, 1000); >); const myPromise2000 = new Promise(function(resolve)< console.log("[myPromise2000] Выполнение асинхронной операции"); setTimeout(()=>, 2000); >); myPromise3000.then((value)=>console.log(value)); myPromise1000.then((value)=>console.log(value)); myPromise2000.then((value)=>console.log(value));
Здесь определены три однотипных промиса. Чтобы каждый из них не выполнялся сразу, они используют функцию setTimeout и устанавливают возвращаемое значение только через несколько секунд. Для разных промисов длительность задержки различается. И в данном случае мы получим следующий консольный вывод:
[myPromise3000] Выполнение асинхронной операции [myPromise1000] Выполнение асинхронной операции [myPromise2000] Выполнение асинхронной операции [myPromise1000] Привет мир! [myPromise2000] Привет мир! [myPromise3000] Привет мир!
Здесь мы видим, что первым начал выполняться промис myPromise3000, однако он же завершился последним, так как для него установлено наибольшее время задержки — 3 секунды. Однако его задержка не помешала выполнению остальных промисов.
При этом нам необязательно вообще передавать в resolve() какое-либо значение. Возможно, асинхронная операция просто выполняется и передает во вне никакого результата.
const x = 4; const y = 8; const myPromise = new Promise(function()< console.log("Выполнение асинхронной операции"); const z = x + y; console.log(`Результат операции: $`) >); myPromise.then();
В данном случае функция в промисе вычисляет сумму чисел x и y и выводит результат на консоль.
Метод Promise.resolve
Иногда требуется просто вернуть из промиса некоторое значение. Для этого можно использовать метод Promise.resolve() . В этот метод передается возвращаемое из промиса значение. Метод Promise.resolve() возвращает объект Promise:
const myPromise = Promise.resolve("Привет мир!"); myPromise.then(value => console.log(value)); // Привет мир!
Определение промиса через функцию
Нередко промис определяется через функцию, которая возвращет объект Promise. Например:
function sum(x, y)< return new Promise(function(resolve)< const result = x + y; resolve(result); >) > sum(3, 5).then(function(value)< console.log("Результат операции:", value);>); sum(25, 4).then(function(value)< console.log("Сумма чисел:", value);>);
Здесь функция sum() принимает два числа и возвращает промис, который инкапсулирует операцию сумму этих чисел. После вычисления сумма чисел передается в resolve() , соответственно мы ее затем можем получить через метод then() . Определение промиса через функцию позволяет нам, с одной стороны, при вызове функции передавать разные значения. А с другой стороны, работать с результатом этой функции как с промисом и настроить при каждом конкретном вызове обработку полученного значения.
Результат работы программы:
Результат операции: 8 Сумма чисел: 29
Однако, что если у нас совпадает принцип обработки полученного из асинхронной функции значения?
sum(3, 5).then(function(value)< console.log("Результат операции:", value);>); sum(25, 4).then(function(value)< console.log("Результат операции:", value);>);
В этом случае логика обработки будет повторяться. Но поскольку метод then() также возвращает объект Promise, то мы можем сделать следующим образом:
function sum(x, y)< return new Promise(function(resolve)< const result = x + y; resolve(result); >).then(function(value)< console.log("Результат операции:", value);>); > sum(3, 5); sum(25, 4);
Гибкая настройка функции
А что, если мы хотим, чтобы у программиста был выбор: если он хочет, то может определить свой обработчик, а если нет, то применяется некоторый обработчик по умолчанию. В этом случае мы можем определить функцию обработчика в качестве параметра функции, а если он не передан, то устанавливать обработчик по умолчанию:
function sum(x, y, func)< // если обработчик не установлен, то устанавливаем обработчик по умолчанию if(func===undefined) func = function(value)< console.log("Результат операции:", value);>; return new Promise(function(resolve)< const result = x + y; resolve(result); >).then(func); > sum(3, 5); sum(25, 4, function(value)< console.log("Сумма:", value);>);
Здесь при первом вызове функции sum() ( sum(3, 5) ) будет срабатывать обработчик по умолчанию. Во втором случае обработчик явным образом передается через третий параметр, соответственно он будет задействован sum(25, 4, function(value)< console.log("Сумма:", value);>)
.then ( )
Обрабатываем ситуацию, когда пообещали значение — и выполнили обещание.
Время чтения: меньше 5 мин
Открыть/закрыть навигацию по статье
Обновлено 29 мая 2022
Эта статья связана с понятием Promise.
Кратко
Скопировать ссылку «Кратко» Скопировано
Метод then ( ) используют, чтобы выполнить код после изменения состояния промиса.
Метод принимает два аргумента:
- on Fulfill — функция-колбэк, которая будет вызвана при переходе промиса в состояние «успех» fulfilled . Функция имеет один параметр, в который передаётся результат выполнения операции
- on Reject — функция-колбэк, которая будет вызвана при переходе промиса в состояние «ошибка» rejected . Функция имеет один параметр, в который передаётся информация об ошибке
Всегда возвращает новый промис.
Как пишется
Скопировать ссылку «Как пишется» Скопировано
// getPasswords() — асинхронная функция, которая возвращает промисgetPasswords().then( function (result) // что-то делаем с результатом операции console.log('Все пароли:' + result) >, function (err) // обрабатываем ошибку console.error(err.message) >)// getPasswords() — асинхронная функция, которая возвращает промис getPasswords().then( function (result) // что-то делаем с результатом операции console.log('Все пароли:' + result) >, function (err) // обрабатываем ошибку console.error(err.message) > )
Как понять
Скопировать ссылку «Как понять» Скопировано
Обработка асинхронных операций через промис и then ( ) очень похожа на работу с колбэками.
Так как then ( ) всегда возвращает новый промис, то его удобно использовать для построения последовательностей асинхронных операций:
// запросим через API список домов из Игры престолов. Метод `fetch` возвращает промисfetch('https://www.anapioficeandfire.com/api/houses') .then(function (response) // выполнится, когда от API придет ответ // запустим асинхронную операцию парсинга JSON из ответа сервера return response.json() // вернем из обработчика промис, к которому добавим then >) .then(function (houses) // выполнится, когда JSON распарсится return fetch(houses[0].overlord) // запросим данные о сюзерене этого дома >) .then(function (response) // выполнится, когда от API придет ответ return response.json() >) .then(function (overlord) console.log(overlord.name) >)// запросим через API список домов из Игры престолов. Метод `fetch` возвращает промис fetch('https://www.anapioficeandfire.com/api/houses') .then(function (response) // выполнится, когда от API придет ответ // запустим асинхронную операцию парсинга JSON из ответа сервера return response.json() // вернем из обработчика промис, к которому добавим then >) .then(function (houses) // выполнится, когда JSON распарсится return fetch(houses[0].overlord) // запросим данные о сюзерене этого дома >) .then(function (response) // выполнится, когда от API придет ответ return response.json() >) .then(function (overlord) console.log(overlord.name) >)
В коде выше, каждый вызов then ( ) привязан к результату предыдущей операции. Такой код читается почти как синхронный.
На практике
Скопировать ссылку «На практике» Скопировано
Николай Лопин советует
Скопировать ссылку «Николай Лопин советует» Скопировано
then ( ) в индустрии используется только для обработки успешного завершения операции, в варианте с одним аргументом:
getPasswords().then(function (result) // что-то делаем с результатом операции console.log(`Все пароли: $`)>)getPasswords().then(function (result) // что-то делаем с результатом операции console.log(`Все пароли: $result>`) >)
Для обработки ошибок используют метод catch ( ) . Такие цепочки читаются лучше:
getPasswords() .then(function (result) console.log(`Все пароли: $`) >) .catch(function (error) console.log(`Ошибка: $`) >)getPasswords() .then(function (result) console.log(`Все пароли: $result>`) >) .catch(function (error) console.log(`Ошибка: $error.message>`) >)
Promise API
В классе Promise есть 6 статических методов. Давайте познакомимся с ними.
Promise.all
Допустим, нам нужно запустить множество промисов параллельно и дождаться, пока все они выполнятся.
Например, параллельно загрузить несколько файлов и обработать результат, когда он готов.
Для этого как раз и пригодится Promise.all .
let promise = Promise.all(iterable);
Метод Promise.all принимает массив промисов (может принимать любой перебираемый объект, но обычно используется массив) и возвращает новый промис.
Новый промис завершится, когда завершится весь переданный список промисов, и его результатом будет массив их результатов.
Например, Promise.all , представленный ниже, выполнится спустя 3 секунды, его результатом будет массив [1, 2, 3] :
Promise.all([ new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1 new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2 new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3 ]).then(alert); // когда все промисы выполнятся, результат будет 1,2,3 // каждый промис даёт элемент массива
Обратите внимание, что порядок элементов массива в точности соответствует порядку исходных промисов. Даже если первый промис будет выполняться дольше всех, его результат всё равно будет первым в массиве.
Часто применяемый трюк – пропустить массив данных через map-функцию, которая для каждого элемента создаст задачу-промис, и затем обернуть получившийся массив в Promise.all .
Например, если у нас есть массив ссылок, то мы можем загрузить их вот так:
let urls = [ 'https://api.github.com/users/iliakan', 'https://api.github.com/users/remy', 'https://api.github.com/users/jeresig' ]; // Преобразуем каждый URL в промис, возвращённый fetch let requests = urls.map(url => fetch(url)); // Promise.all будет ожидать выполнения всех промисов Promise.all(requests) .then(responses => responses.forEach( response => alert(`$: $`) ));
А вот пример побольше, с получением информации о пользователях GitHub по их логинам из массива (мы могли бы получать массив товаров по их идентификаторам, логика та же):
let names = ['iliakan', 'remy', 'jeresig']; let requests = names.map(name => fetch(`https://api.github.com/users/$`)); Promise.all(requests) .then(responses => < // все промисы успешно завершены for(let response of responses) < alert(`$: $`); // покажет 200 для каждой ссылки > return responses; >) // преобразовать массив ответов response в response.json(), // чтобы прочитать содержимое каждого .then(responses => Promise.all(responses.map(r => r.json()))) // все JSON-ответы обработаны, users - массив с результатами .then(users => users.forEach(user => alert(user.name)));
Если любой из промисов завершится с ошибкой, то промис, возвращённый Promise.all , немедленно завершается с этой ошибкой.
Promise.all([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ошибка!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).catch(alert); // Error: Ошибка!
Здесь второй промис завершится с ошибкой через 2 секунды. Это приведёт к немедленной ошибке в Promise.all , так что выполнится .catch : ошибка этого промиса становится ошибкой всего Promise.all .
В случае ошибки, остальные результаты игнорируются
Если один промис завершается с ошибкой, то весь Promise.all завершается с ней, полностью забывая про остальные промисы в списке. Их результаты игнорируются.
Например, если сделано несколько вызовов fetch , как в примере выше, и один не прошёл, то остальные будут всё ещё выполняться, но Promise.all за ними уже не смотрит. Скорее всего, они так или иначе завершатся, но их результаты будут проигнорированы.
Promise.all ничего не делает для их отмены, так как в промисах вообще нет концепции «отмены». В главе Fetch: прерывание запроса мы рассмотрим AbortController , который помогает с этим, но он не является частью Promise API.
Promise.all(iterable) разрешает передавать не-промисы в iterable (перебираемом объекте)
Обычно, Promise.all(. ) принимает перебираемый объект промисов (чаще всего массив). Но если любой из этих объектов не является промисом, он передаётся в итоговый массив «как есть».
Например, здесь результат: [1, 2, 3]
Promise.all([ new Promise((resolve, reject) => < setTimeout(() =>resolve(1), 1000) >), 2, 3 ]).then(alert); // 1, 2, 3
Таким образом, мы можем передавать уже готовые значения, которые не являются промисами, в Promise.all , иногда это бывает удобно.
Promise.allSettled
Новая возможность
Эта возможность была добавлена в язык недавно. В старых браузерах может понадобиться полифил.
let promise = Promise.allSettled(iterable);
Promise.all завершается с ошибкой, если она возникает в любом из переданных промисов. Это подходит для ситуаций «всё или ничего», когда нам нужны все результаты для продолжения:
Promise.all([ fetch('/template.html'), fetch('/style.css'), fetch('/data.json') ]).then(render); // методу render нужны результаты всех fetch
Метод Promise.allSettled всегда ждёт завершения всех промисов. В массиве результатов будет
Например, мы хотели бы загрузить информацию о множестве пользователей. Даже если в каком-то запросе ошибка, нас всё равно интересуют остальные.
Используем для этого Promise.allSettled :
let urls = [ 'https://api.github.com/users/iliakan', 'https://api.github.com/users/remy', 'https://no-such-url' ]; Promise.allSettled(urls.map(url => fetch(url))) .then(results => < // (*) results.forEach((result, num) =>< if (result.status == "fulfilled") < alert(`$: $`); > if (result.status == "rejected") < alert(`$: $`); > >); >);
Массив results в строке (*) будет таким:
То есть, для каждого промиса у нас есть его статус и значение/ошибка.
Полифил
Если браузер не поддерживает Promise.allSettled , для него легко сделать полифил:
if(!Promise.allSettled) < Promise.allSettled = function(promises) < return Promise.all(promises.map(p =>Promise.resolve(p).then(value => (< status: 'fulfilled', value: value >), error => (< status: 'rejected', reason: error >)))); >; >
В этом коде promises.map берёт аргументы, превращает их в промисы (на всякий случай) и добавляет каждому обработчик .then .
Этот обработчик превращает успешный результат value в , а ошибку error в . Это как раз и есть формат результатов Promise.allSettled .
Затем мы можем использовать Promise.allSettled , чтобы получить результаты всех промисов, даже если при выполнении какого-то возникнет ошибка.
Promise.race
Метод очень похож на Promise.all , но ждёт только первый выполненный промис, из которого берёт результат (или ошибку).
let promise = Promise.race(iterable);
Например, тут результат будет 1 :
Promise.race([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ошибка!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(alert); // 1
Быстрее всех выполнился первый промис, он и дал результат. После этого остальные промисы игнорируются.
Promise.any
Метод очень похож на Promise.race , но ждёт только первый успешно выполненный промис, из которого берёт результат.
Если ни один из переданных промисов не завершится успешно, тогда возвращённый объект Promise будет отклонён с помощью AggregateError – специального объекта ошибок, который хранит все ошибки промисов в своём свойстве errors .
let promise = Promise.any(iterable);
Например, здесь, результатом будет 1 :
Promise.any([ new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ошибка!")), 1000)), new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(alert); // 1
Первый промис в этом примере был самым быстрым, но он был отклонён, поэтому результатом стал второй. После того, как первый успешно выполненный промис «выиграет гонку», все дальнейшие результаты будут проигнорированы.
Вот пример, в котором все промисы отклоняются:
Promise.any([ new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ошибка!")), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ещё одна ошибка!")), 2000)) ]).catch(error => < console.log(error.constructor.name); // AggregateError console.log(error.errors[0]); // Error: Ошибка! console.log(error.errors[1]); // Error: Ещё одна ошибка! >);
Как вы можете видеть, объекты ошибок для отклонённых промисов доступны в свойстве errors объекта AggregateError .
Promise.resolve/reject
Методы Promise.resolve и Promise.reject редко используются в современном коде, так как синтаксис async/await (мы рассмотрим его чуть позже) делает их, в общем-то, не нужными.
Мы рассмотрим их здесь для полноты картины, а также для тех, кто по каким-то причинам не может использовать async/await .
Promise.resolve
- Promise.resolve(value) создаёт успешно выполненный промис с результатом value .
То же самое, что:
let promise = new Promise(resolve => resolve(value));
Этот метод используют для совместимости: когда ожидается, что функция возвратит именно промис.
Например, функция loadCached ниже загружает URL и запоминает (кеширует) его содержимое. При будущих вызовах с тем же URL он тут же читает предыдущее содержимое из кеша, но использует Promise.resolve , чтобы сделать из него промис, для того, чтобы возвращаемое значение всегда было промисом:
let cache = new Map(); function loadCached(url) < if (cache.has(url)) < return Promise.resolve(cache.get(url)); // (*) >return fetch(url) .then(response => response.text()) .then(text => < cache.set(url,text); return text; >); >
Мы можем писать loadCached(url).then(…) , потому что функция loadCached всегда возвращает промис. Мы всегда можем использовать .then после loadCached . Это и есть цель использования Promise.resolve в строке (*) .
Promise.reject
- Promise.reject(error) создаёт промис, завершённый с ошибкой error .
То же самое, что:
let promise = new Promise((resolve, reject) => reject(error));
На практике этот метод почти никогда не используется.
Итого
Мы ознакомились с шестью статическими методами класса Promise :
- Promise.all(promises) – ожидает выполнения всех промисов и возвращает массив с результатами. Если любой из указанных промисов вернёт ошибку, то результатом работы Promise.all будет эта ошибка, результаты остальных промисов будут игнорироваться.
- Promise.allSettled(promises) (добавлен недавно) – ждёт, пока все промисы завершатся и возвращает их результаты в виде массива с объектами, у каждого объекта два свойства:
- status : «fulfilled» , если выполнен успешно или «rejected» , если ошибка,
- value – результат, если успешно или reason – ошибка, если нет.
- Promise.race(promises) – ожидает первый выполненный промис, который становится его результатом, остальные игнорируются.
- Promise.any(promises) (добавлен недавно) – ожидает первый успешно выполненный промис, который становится его результатом, остальные игнорируются. Если все переданные промисы отклонены, AggregateError становится ошибкой Promise.any .
- Promise.resolve(value) – возвращает успешно выполнившийся промис с результатом value .
- Promise.reject(error) – возвращает промис с ошибкой error .
Из всех перечисленных методов, самый часто используемый – это, пожалуй, Promise.all .
Цепочка промисов
Давайте вернёмся к ситуации из главы Введение: колбэки: у нас есть последовательность асинхронных задач, которые должны быть выполнены одна за другой. Например, речь может идти о загрузке скриптов. Как же грамотно реализовать это в коде?
Промисы предоставляют несколько способов решения подобной задачи.
В этой главе мы разберём цепочку промисов.
Она выглядит вот так:
new Promise(function(resolve, reject) < setTimeout(() =>resolve(1), 1000); // (*) >).then(function(result) < // (**) alert(result); // 1 return result * 2; >).then(function(result) < // (***) alert(result); // 2 return result * 2; >).then(function(result) < alert(result); // 4 return result * 2; >);
Идея состоит в том, что результат первого промиса передаётся по цепочке обработчиков .then .
Поток выполнения такой:
- Начальный промис успешно выполняется через 1 секунду (*) ,
- Затем вызывается обработчик в .then (**) .
- Возвращаемое им значение передаётся дальше в следующий обработчик .then (***)
- …и так далее.
В итоге результат передаётся по цепочке обработчиков, и мы видим несколько alert подряд, которые выводят: 1 → 2 → 4 .
Всё это работает, потому что вызов promise.then тоже возвращает промис, так что мы можем вызвать на нём следующий .then .
Когда обработчик возвращает какое-то значение, то оно становится результатом выполнения соответствующего промиса и передаётся в следующий .then .
Классическая ошибка новичков: технически возможно добавить много обработчиков .then к единственному промису. Но это не цепочка.
let promise = new Promise(function(resolve, reject) < setTimeout(() =>resolve(1), 1000); >); promise.then(function(result) < alert(result); // 1 return result * 2; >); promise.then(function(result) < alert(result); // 1 return result * 2; >); promise.then(function(result) < alert(result); // 1 return result * 2; >);
Мы добавили несколько обработчиков к одному промису. Они не передают друг другу результаты своего выполнения, а действуют независимо.
Вот картина происходящего (сравните это с изображением цепочки промисов выше):
Все обработчики .then на одном и том же промисе получают одно и то же значение – результат выполнения того же самого промиса. Таким образом, в коде выше все alert показывают одно и то же: 1 .
На практике весьма редко требуется назначать несколько обработчиков одному промису. А вот цепочка промисов используется куда чаще.
Возвращаем промисы
Обработчик handler , переданный в .then(handler) , может вернуть промис.
В этом случае дальнейшие обработчики ожидают, пока он выполнится, и затем получают его результат.
new Promise(function(resolve, reject) < setTimeout(() =>resolve(1), 1000); >).then(function(result) < alert(result); // 1 return new Promise((resolve, reject) => < // (*) setTimeout(() =>resolve(result * 2), 1000); >); >).then(function(result) < // (**) alert(result); // 2 return new Promise((resolve, reject) => < setTimeout(() =>resolve(result * 2), 1000); >); >).then(function(result) < alert(result); // 4 >);
Здесь первый .then показывает 1 и возвращает новый промис new Promise(…) в строке (*) . Через одну секунду этот промис успешно выполняется, и его результат (аргумент в resolve , то есть result * 2 ) передаётся обработчику в следующем .then . Он находится в строке (**) , показывает 2 и делает то же самое.
Таким образом, как и в предыдущем примере, выводятся 1 → 2 → 4, но сейчас между вызовами alert существует пауза в 1 секунду.
Возвращая промисы, мы можем строить цепочки из асинхронных действий.
Пример: loadScript
Давайте используем эту возможность вместе с промисифицированной функцией loadScript , созданной нами в предыдущей главе, чтобы загружать скрипты по очереди, последовательно:
loadScript("/article/promise-chaining/one.js") .then(function(script) < return loadScript("/article/promise-chaining/two.js"); >) .then(function(script) < return loadScript("/article/promise-chaining/three.js"); >) .then(function(script) < // вызовем функции, объявленные в загружаемых скриптах, // чтобы показать, что они действительно загрузились one(); two(); three(); >);
Этот же код можно переписать немного компактнее, используя стрелочные функции:
loadScript("/article/promise-chaining/one.js") .then(script => loadScript("/article/promise-chaining/two.js")) .then(script => loadScript("/article/promise-chaining/three.js")) .then(script => < // скрипты загружены, мы можем использовать объявленные в них функции one(); two(); three(); >);
Здесь каждый вызов loadScript возвращает промис, и следующий обработчик в .then срабатывает, только когда этот промис завершается. Затем инициируется загрузка следующего скрипта и так далее. Таким образом, скрипты загружаются один за другим.
Мы можем добавить и другие асинхронные действия в цепочку. Обратите внимание, что наш код всё ещё «плоский», он «растёт» вниз, а не вправо. Нет никаких признаков «адской пирамиды вызовов».
Технически мы бы могли добавлять .then напрямую к каждому вызову loadScript , вот так:
loadScript("/article/promise-chaining/one.js").then(script1 => < loadScript("/article/promise-chaining/two.js").then(script2 => < loadScript("/article/promise-chaining/three.js").then(script3 =>< // эта функция имеет доступ к переменным script1, script2 и script3 one(); two(); three(); >); >); >);
Этот код делает то же самое: последовательно загружает 3 скрипта. Но он «растёт вправо», так что возникает такая же проблема, как и с колбэками.
Разработчики, которые не так давно начали использовать промисы, иногда не знают про цепочки и пишут код именно так, как показано выше. В целом, использование цепочек промисов предпочтительнее.
Иногда всё же приемлемо добавлять .then напрямую, чтобы вложенная в него функция имела доступ к внешней области видимости. В примере выше самая глубоко вложенная функция обратного вызова имеет доступ ко всем переменным script1 , script2 , script3 . Но это скорее исключение, чем правило.
Если быть более точными, обработчик может возвращать не именно промис, а любой объект, содержащий метод .then , такие объекты называют «thenable», и этот объект будет обработан как промис.
Смысл в том, что сторонние библиотеки могут создавать свои собственные совместимые с промисами объекты. Они могут иметь свои наборы методов и при этом быть совместимыми со встроенными промисами, так как реализуют метод .then .
Вот пример такого объекта:
class Thenable < constructor(num) < this.num = num; >then(resolve, reject) < alert(resolve); // function() < native code >// будет успешно выполнено с аргументом this.num*2 через 1 секунду setTimeout(() => resolve(this.num * 2), 1000); // (**) > > new Promise(resolve => resolve(1)) .then(result => < return new Thenable(result); // (*) >) .then(alert); // показывает 2 через 1000мс
JavaScript проверяет объект, возвращаемый из обработчика .then в строке (*) : если у него имеется метод then , который можно вызвать, то этот метод вызывается, и в него передаются как аргументы встроенные функции resolve и reject , вызов одной из которых потом ожидается. В примере выше происходит вызов resolve(2) через 1 секунду (**) . Затем результат передаётся дальше по цепочке.
Это позволяет добавлять в цепочки промисов пользовательские объекты, не заставляя их наследовать от Promise .
Более сложный пример: fetch
Во фронтенд-разработке промисы часто используются, чтобы делать запросы по сети. Давайте рассмотрим один такой пример.
Мы будем использовать метод fetch, чтобы подгрузить информацию о пользователях с удалённого сервера. Этот метод имеет много опциональных параметров, разобранных в соответствующих разделах, но базовый синтаксис весьма прост:
let promise = fetch(url);
Этот код запрашивает по сети url и возвращает промис. Промис успешно выполняется и в свою очередь возвращает объект response после того, как удалённый сервер присылает заголовки ответа, но до того, как весь ответ сервера полностью загружен.
Чтобы прочитать полный ответ, надо вызвать метод response.text() : он тоже возвращает промис, который выполняется, когда данные полностью загружены с удалённого сервера, и возвращает эти данные.
Код ниже запрашивает файл user.json и загружает его содержимое с сервера:
fetch('/article/promise-chaining/user.json') // .then в коде ниже выполняется, когда удалённый сервер отвечает .then(function(response) < // response.text() возвращает новый промис, // который выполняется и возвращает полный ответ сервера, // когда он загрузится return response.text(); >) .then(function(text) < // . и здесь содержимое полученного файла alert(text); // >);
Есть также метод response.json() , который читает данные в формате JSON. Он больше подходит для нашего примера, так что давайте использовать его.
Мы также применим стрелочные функции для более компактной записи кода:
// то же самое, что и раньше, только теперь response.json() читает данные в формате JSON fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => alert(user.name)); // iliakan, получили имя пользователя
Теперь давайте что-нибудь сделаем с полученными данными о пользователе.
Например, мы можем послать запрос на GitHub, чтобы загрузить данные из профиля пользователя и показать его аватар:
// Запрашиваем user.json fetch('/article/promise-chaining/user.json') // Загружаем данные в формате json .then(response => response.json()) // Делаем запрос к GitHub .then(user => fetch(`https://api.github.com/users/$`)) // Загружаем ответ в формате json .then(response => response.json()) // Показываем аватар (githubUser.avatar_url) в течение 3 секунд (возможно, с анимацией) .then(githubUser => < let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() =>img.remove(), 3000); // (*) >);
Код работает, детали реализации отражены в комментариях. Однако в нём есть одна потенциальная проблема, с которой часто сталкиваются новички.
Посмотрите на строку (*) : как мы можем предпринять какие-то действия после того, как аватар был показан и удалён? Например, мы бы хотели показывать форму редактирования пользователя или что-то ещё. Сейчас это невозможно.
Чтобы сделать наш код расширяемым, нам нужно возвращать ещё один промис, который выполняется после того, как завершается показ аватара.
fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => fetch(`https://api.github.com/users/$`)) .then(response => response.json()) .then(githubUser => new Promise(function(resolve, reject) < // (*) let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() =>< img.remove(); resolve(githubUser); // (**) >, 3000); >)) // срабатывает через 3 секунды .then(githubUser => alert(`Закончили показ $`));
То есть, обработчик .then в строке (*) будет возвращать new Promise , который перейдёт в состояние «выполнен» только после того, как в setTimeout (**) будет вызвана resolve(githubUser) .
Соответственно, следующий по цепочке .then будет ждать этого.
Как правило, все асинхронные действия должны возвращать промис.
Это позволяет планировать после него какие-то дополнительные действия. Даже если эта возможность не нужна прямо сейчас, она может понадобиться в будущем.
И, наконец, давайте разобьём написанный код на отдельные функции, пригодные для повторного использования:
function loadJson(url) < return fetch(url) .then(response =>response.json()); > function loadGithubUser(name) < return fetch(`https://api.github.com/users/$`) .then(response => response.json()); > function showAvatar(githubUser) < return new Promise(function(resolve, reject) < let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() =>< img.remove(); resolve(githubUser); >, 3000); >); > // Используем их: loadJson('/article/promise-chaining/user.json') .then(user => loadGithubUser(user.name)) .then(showAvatar) .then(githubUser => alert(`Показ аватара $ завершён`)); // .
Итого
Если обработчик в .then (или в catch/finally , без разницы) возвращает промис, последующие элементы цепочки ждут, пока этот промис выполнится. Когда это происходит, результат его выполнения (или ошибка) передаётся дальше.
Вот полная картина происходящего:
