Javascript метод promise.then()
Содержание:
- Плюсы и минусы в теории
- Bigger example: fetch
- Waiting for a file
- Концепция 2: Цепочка промисов
- Конструктор Promise, его философия. Callback функция executor — как «выполнитель» обещания. Схема взаимодействия: Promise ( конструктор ) — executor ( callback ) — promise ( объект )
- Популярные ошибки и подводные камни
- Creating a promise: the Promise constructor
- Алгоритм создания promise объекта по спецификации ECMAScript
- Новичок на районе: Observables
- Example: loadScript
- Требование заказчика
- JavaScript Promise Object
- Понимание промисов
- JavaScript Promise Chaining
- Example: loadScript
- Сложный кейс с промисами, и главное преимущество промисов — колбеки
- Promise.any
- Итого
Плюсы и минусы в теории
Async/awaitПлюсы
- Удобство и простота чтения
- Возможность использования последовательного стиля программирования
Минусы
- Легко наткнуться на избыточное ожидание последовательного кода. Для истинной параллельности нужно модифицировать код.
- Неочевидность возвращаемых значений try…catch.
PromiseПлюсы
- Использует традиционный подход колбэков.
- Данные с ошибками и данные с успешным результатом операции однозначно понимаемы.
- Возможность использовать Promise.all без оглядки на синтаксис.
- Оповещения Promise.resolve и Promise.reject доступны везде.
- Наглядное использование метода Promise.finally.
Минусы
При неправильном использовании возможно создание слишком глубоких использований цепочек .then
Bigger example: fetch
In frontend programming promises are often used for network requests. So let’s see an extended example of that.
We’ll use the fetch method to load the information about the user from the remote server. It has a lot of optional parameters covered in separate chapters, but the basic syntax is quite simple:
This makes a network request to the and returns a promise. The promise resolves with a object when the remote server responds with headers, but before the full response is downloaded.
To read the full response, we should call the method : it returns a promise that resolves when the full text is downloaded from the remote server, with that text as a result.
The code below makes a request to and loads its text from the server:
The object returned from also includes the method that reads the remote data and parses it as JSON. In our case that’s even more convenient, so let’s switch to it.
We’ll also use arrow functions for brevity:
Now let’s do something with the loaded user.
For instance, we can make one more request to GitHub, load the user profile and show the avatar:
The code works; see comments about the details. However, there’s a potential problem in it, a typical error for those who begin to use promises.
Look at the line : how can we do something after the avatar has finished showing and gets removed? For instance, we’d like to show a form for editing that user or something else. As of now, there’s no way.
To make the chain extendable, we need to return a promise that resolves when the avatar finishes showing.
Like this:
That is, the handler in line now returns , that becomes settled only after the call of in . The next in the chain will wait for that.
As a good practice, an asynchronous action should always return a promise. That makes it possible to plan actions after it; even if we don’t plan to extend the chain now, we may need it later.
Finally, we can split the code into reusable functions:
Waiting for a file
Example using Callback
function getFile(myCallback) {
let req = new XMLHttpRequest();
req.open(‘GET’, «mycar.html»);
req.onload = function() {
if (req.status == 200) {
myCallback(req.responseText);
} else {
myCallback(«Error: » + req.status);
}
}
req.send();
}
getFile(myDisplayer);
Example using Promise
let myPromise = new Promise(function(myResolve, myReject) {
let req = new XMLHttpRequest();
req.open(‘GET’, «mycar.htm»);
req.onload = function() {
if (req.status == 200) {
myResolve(req.response);
} else {
myReject(«File not Found»);
}
};
req.send();
});
myPromise.then(
function(value) {myDisplayer(value);},
function(error) {myDisplayer(error);}
);
Концепция 2: Цепочка промисов
Метод then возвращает новый промис, который можно использовать для дальнейшего объединения в цепочку.
Предположим, что нужно использовать несколько обратных вызовов для конкретного промиса и обработать результат один за другим.
Цепочка вызовов может быть реализована двумя способами:
Использование отдельного обработчика ошибок для каждого успешного обратного вызова – при этом метод then принимает два аргумента: один для успешного обратного вызова,а другой — для неудачного:
timer().then(successHandler1, failureHandler1). .then(successHandler2, failureHandler2) .then(successHandler3, failuerHandler3) .then(successHandler4, failureHandler4)
Использование стандартного обработчика ошибок – обратный вызов failureHandlerне является обязательным в методе then. Поэтому можно использовать блок catch для общей обработки ошибок:
timer().then(successHandler1) .then(successHandler2) .then(successHandler3) .catch(errorHandler)
В приведенном выше примере можно передать параметр через разные обратные вызовы и увеличивать их значение.
Если мы возвращаем error из любого обратного вызова then, то для последующих методов then обратный вызов error будет выполнен. После чего ошибка будет передана в последний блок catch.
Конструктор Promise, его философия. Callback функция executor — как «выполнитель» обещания. Схема взаимодействия: Promise ( конструктор ) — executor ( callback ) — promise ( объект )
Итак, мы выяснили, что promise — это сущность, которая технически представляет собой JS объект с особыми скрытыми внутренними полями, которые в свою очередь обеспечивают философское наполнение смыслом слова «обещание».
Когда новичок первый раз создает объект promise, то его ожидает следующая картина (рис. 5).
рис 5. ( Самый первый раз интуитивно создаем promise объект )
Что пошло не так, и почему ошибка — стандартный вопрос. При ответе на него лучше снова привести какую-то аналогию из жизни. Например, мало кто любит «пустозвонов» вокруг нас: которые только обещают, но ничего не делают по выполнению своих заявлений (политика не в счет). Мы намного лучше относимся к тем людям, которые после своего обещания имеют план и предпринимают сразу же какие-то действия для достижения обещанного результата.
Так и философия ECMAScript подразумевает, что если вы создаете обещание, то сразу же укажите, как вы его будете выполнять. Свой план действий программисту необходимо оформить в виде параметра-функции, которую передадите в конструктор Promise. Следующий эксперимент выглядит так (рис. 6).
рис 6. ( Создаем promise объект, передавая в конструктор Promise функцию executor )
Из подписи к рисунку мы видим, что функция (параметр конструктора Promise) имеет собственное название — executor. Её задача — начать выполнение обещания и, желательно, привести его к какому-то логическому завершению. И если программист может писать какой угодно код в executor-е, то как программисту просигнализировать JS-у, что все — работа сделана — можно идти и смотреть результаты обещания?
Маркеры или сигналы, которые помогут программисту сообщить, что обещание завершено, передаются автоматически в параметры executor-a в виде аргументов, специально сформированных JavaScript-ом. Эти параметры можно называть как угодно, но чаще всего вы встретите их под такими именами, как res и rej. В спецификации ECMAScript их полное название — resolve function и reject function. Эти маркеры-функции имеют свои особенности, которые рассмотрим чуть ниже.
Для осознания новой информации новичку предлагается самостоятельно закодировать следующее утверждение: «Обещаю, что смогу разделить одно число на другое и выдать ответ, если только делитель не ноль». Вот как будет выглядеть приблизительно такой код (рис. 7).
рис 7. ( Решение задачи на деление 2-х чисел через промисы )
Теперь можно проанализировать полученный результат. Мы видим, что уже второй раз консоль браузера показывает объект промис в интересном виде. А именно: указаны 2 дополнительных поля в двойных квадратных скобках. Можно спокойно провести аналогию между `PromiseState` и `PromiseStatus`, fulfilled и resolved, `PromiseValue` и `PromiseResult`. Да, браузер сам пытается подсказать программисту о наличии и значении внутренних полей promise объекта. Также мы видим воедино связанную систему объекта promise, функции executor, специальных функций-callback-маркеров res и rej.
Чтобы ученик / напарник раскрепостился еще больше в этом материале, ему предлагается следующий код (рис. 8). Необходимо его проанализировать и ответить на следующие вопросы.
рис 8. ( Вариация решения задачи на деление 2-х чисел через промисы )
Отработает ли код? Где здесь функция executor и какое она имеет имя? Подходящее ли в этом коде название «wantToDivide»? Что возвращает после себя функция bind? Почему в функцию bind аргументы передаются только на втором и третьем месте? Куда исчезли специальные функции resolve function и reject function? Каким образом необходимые вводные данные number1 и number2 попали в «план выполнения обещания»? Сколько элементов в псевдомассиве «arguments»? Можно ли по памяти восстановить то, как будет выглядеть ответ в консоли браузера?
Читателю предлагается самому подумать над ответами на вопросы. А также
поэкспериментировать в коде. Благо код небольшой и сама идея задачи — простая. Да, тут есть вопросы как на промисы, так и на общие знания JavaScript. Что поделать, везде нас поджидают неожиданности, которые не дают нам расслабиться. Как только вам станет все понятно — можно двигаться дальше.
Популярные ошибки и подводные камни
Из-за сложных манипуляций с промисами и async/await концепциями вы можете встретиться с различными тонкостями, что может привести к ошибкам.
Не забывайте await
Частая ошибка заключается в том, что перед промисом забывается ключевое слово :
Обратите внимание, здесь не используется ни , ни. Функция всегда будет завершаться с (без задержки в 1 секунду)
Тем не менее, промис будет выполняться. Если промис будет выдавать ошибку либо реджект, то будет вызываться .
async-функции в обратных вызовах
async-функции часто используются в . или . в качестве коллбэков. Вот пример — допустим, существует функция , которая возвращает количество открытых репозиториев на GitHub. Есть 3 пользователя, чьи показатели нужно взять. Используется такой код:
И для того, чтобы получить количество репозиториев пользователей (), код должен выглядеть как-то так:
Обратите внимание на слово в обратном вызове функции. Можно было бы ожидать, что переменная будет содержать число — количество репозиториев
Но как было сказано ранее, все async-функции возвращают промисы. Следовательно, будет массивом промисов. вызывает анонимной коллбэк для каждого пользователя.
Слишком последовательное использование await
Допустим, есть такой код:
В переменную помещается количество репозиториев, потом это количество добавляется в массив . Проблема этого кода в том, что пока с сервера не придут данные первого пользователя, все последующие пользователи будут находиться в ожидании. Получается, что в один момент времени обрабатывается только один пользователь.
Если на обработку одного пользователя будет уходить 300 мс, то на всех пользователей уйдёт почти секунда. В этом случае затрачиваемое время будет линейно зависеть от количества пользователей. Поскольку получение количества репозиториев не зависит друг от друга, то можно распараллелить эти процессы. Тогда пользователи будут обрабатываться одновременно, а не последовательно. Для этого понадобятся и .
на входе получает массив промисов и возвращает промис. Возвращаемый промис завершается после окончания всех промисов в массиве либо при первом реджекте. Возможно, все эти промисы не запустятся строго одновременно. Чтобы добиться строгого параллелизма, взгляните на p-map. А если нужно, чтобы async-функции были более адаптивными, посмотрите на Async Iterators.
Перевод статьи «Deeply Understanding JavaScript Async and Await with Examples»
Creating a promise: the Promise constructor
To create a promise in JavaScript, you use the constructor:
The constructor accepts a function as an argument. This function is called the .
The executor accepts two functions with the names, by convention, and .
When you call the , the is called automatically.
Inside the executor, you manually call the function if the executor is completed successfully and invoke the function in case of an error occurs.
If you embed the above JavaScript code in an HTML document and check the console window, you will see that the promise is resolved because the variable is set to .
To see the pending state of the promise, we wrap the code of the executor in the function:
Now, you see that the promise starts with the state with the value is . The promise value will be returned later once the promise is completed.
After about 3 seconds, type the in the console window, you will see that the state of the promise becomes and the promise value is the string that we passed to the function.
So calling the function moves the promise object to the fulfilled state. If you change the value of the variable to and run the script again:
You will see an error message and the state of the promise becomes after 3 seconds:
In other words, calling the method moves the promise object to the state.
The following picture illustrates the states of a promise and the effect of calling the and functions:
Once the promise reaches either fulfilled state or rejected state, it stays in that state and can’t switch.
In other words, a promise cannot go from the fulfilled state to the rejected state and vice versa. It also cannot go back from the fulfilled state or rejected state to the pending state.
If the promise reaches fulfilled state or rejected state, the promise is resolved.
Once a new object is created, it is in the pending state until it is resolved. To schedule a callback when the promise is either resolved or rejected, you call the methods of the object: , , and .
Алгоритм создания promise объекта по спецификации ECMAScript
Рассмотрим тот завораживающий момент, когда на свет рождается он — полноценный (рис. 16).
рис 16. ( Алгоритм создания promise объекта из EcmaScript спецификации )
Никаких сложных вопросов при его просмотре возникать уже не должно:
- конструктор Promise должен быть вызван обязательно в режиме конструктора, а не обычного вызова функции
- конструктор Promise требует наличия executor функции
- создаем JavaScript объект со специфическими скрытыми полями
- инициализируем скрытые поля какими-то начальными значениями
- создаем связанные с promise объектом функции resolve и reject
- вызываем на исполнение executor функцию, передавая туда в качестве аргументов уже сформированные маркеры resolve function и reject function
- если в процессе выполнения executor-a что-то пошло не так, переводим наш promise объект в состояние rejected
- возвращаем в переменную родившийся на свет promise объект.
Не знаю, стало ли для вас открытием, что алгоритм executor функции выполняется здесь и сейчас, в обычном синхронном режиме, еще до того, как что-то будет записано в переменную слева от конструктора Promise. Но в свое время для меня это стало откровением.
Раз уж затронули тему синхронности и асинхронности, то вот вам следующий код «на подумать» или для экспериментов. Вопрос: просмотрев некое творение программиста Димы, сможете ли вы ответить, какой смысл игры закодирован ниже?
Конечно же, это эмуляция броска игрального кубика. Сможет пользователь угадать выпавшее число или нет? Посмотрите как органично асинхронный setTimeout встраивается в синхронный executor — в наш план бросить кубик и узнать выпавшее число. Как можно уже по-особому интерпретировать результаты в консоли разработчика (рис. 17)?
Если мы попробуем посмотреть промис до того времени, как кубик остановится (3000 мс указано в коде), то мы увидим, что промис до сих пор находится в состоянии ожидания: игра не завершилась, кубик еще не остановился, выпавшего числа нет. Если же мы попробуем посмотреть promise объект после остановки кубика, то мы увидим вполне конкретную информацию: выиграл ли пользователь (угадав число), или проиграл и почему (какое число выпало на самом деле).
рис 17. ( Состояние promise объекта при наличие асинхронной операции в executor функции )
Если вас заинтересовал данный пример, или вы хотите угадать выпавшее число подброшенного кубика — можете скопировать код и провести свои эксперименты. Смелей!
Новичок на районе: Observables
Перед тем как закончить с промисами, есть кое-что, что пришло для того, чтобы облегчить работу с асинхронными данными — это Observables.
— это ленивые потоки событий, которые могут выдать ноль или больше событий, а могут и вообще не закончиться.
Некоторые ключевые различия между промисами и observables:
Они отменяемые
Они ленивы/медленны
Давайте посмотрим на тоже самое демо, только написанное с помощью . В этом примере, я использую RxJS.
конвертит промис в поток
и среди некоторых операторов доступных для
Потоки ленивы. Наш запускается, когда мы на него.
могут делать много забавных вещей довольно легко. Для примера, delay добавляет функцию за 3 секунды с всего-лишь одной строкой кода или пробовать заново, так что вы можете делать запрос определенное количество раз.
Example: loadScript
We’ve got the function for loading a script from the previous chapter.
Here’s the callback-based variant, just to remind us of it:
Let’s rewrite it using Promises.
The new function will not require a callback. Instead, it will create and return a Promise object that resolves when the loading is complete. The outer code can add handlers (subscribing functions) to it using :
Usage:
We can immediately see a few benefits over the callback-based pattern:
Promises | Callbacks |
---|---|
Promises allow us to do things in the natural order. First, we run , and we write what to do with the result. | We must have a function at our disposal when calling . In other words, we must know what to do with the result before is called. |
We can call on a Promise as many times as we want. Each time, we’re adding a new “fan”, a new subscribing function, to the “subscription list”. More about this in the next chapter: Promises chaining. | There can be only one callback. |
So promises give us better code flow and flexibility. But there’s more. We’ll see that in the next chapters.
Требование заказчика
История закончилась бы замечательно, если бы не одно «но». ТЗ требовало, чтобы весь код был написан на async/await. Глядя на код выше можно сказать, что это достаточно сложный кейс. Первое, что можно подумать: «Это невозможно! Ведь async/await не могут ждать колбека, они только выполняют код и ничего не ждут.»
Ну хорошо… требование заказчика — закон… переписываем.
Задача действительно оказалась не решаема на уровне async/await. Потому что , и не работают в связке. Пришлось совсем чуть-чуть смешать два разных синтаксиса с помощью функции . Хорошо это или плохо? Вопрос субъективный. Мы лишь в очередной раз убедились, что async/await является лишь надстройкой над промисами.
JavaScript Promise Object
A JavaScript Promise object contains both the producing code and calls to the consuming code:
Promise Syntax
let myPromise = new Promise(function(myResolve, myReject) {
// «Producing Code» (May take some time)
myResolve(); // when successful
myReject(); // when error
});
// «Consuming Code» (Must wait for a fulfilled Promise)
myPromise.then(
function(value) { /* code if successful */ },
function(error) { /* code if some error */ }
);
When the executing code obtains the result, it should call one of the two callbacks:
Result | Call |
---|---|
Success | myResolve(result value) |
Error | myReject(error object) |
Понимание промисов
Итак, вкратце про промисы: “Представьте, что вы ребенок. Ваша мама обещает вам, что вы получите новый телефон на следующей неделе.”
Вы не знаете, получите ли вы его до следующей неделе. Ваша мама может купить вам совершенно новый телефон, а может просто этого не сделать, к примеру, потому что она будет не в настроении
Это и есть промис. От английского promise — обещать. Небольшое уточнение, пожалуйста, не усложняйте понимание другим с произношением, так как во всём русскоязычном мире принято говорить “промис” в случае с JavaScript.
Итак, у промиса есть 3 состояния. Это:
1. Промис в состоянии ожидания (). Когда вы не знаете, получите ли вы мобильный телефон к следующей неделе или нет.
2. Промис решен (). Вам реально купят новый телефон.
3. Промис отклонен (). Вы не получили новый мобильный телефон, так как всё-таки, мама была не в настроении.
JavaScript Promise Chaining
Promises are useful when you have to handle more than one asynchronous task, one after another. For that, we use promise chaining.
You can perform an operation after a promise is resolved using methods , and .
The method is used with the callback when the promise is successfully fulfilled or resolved.
The syntax of method is:
Example 2: Chaining the Promise with then()
Output
Promise resolved You can call multiple functions this way.
In the above program, the method is used to chain the functions to the promise. The method is called when the promise is resolved successfully.
You can chain multiple methods with the promise.
JavaScript catch() method
The method is used with the callback when the promise is rejected or if an error occurs. For example,
Output
Promise rejected
In the above program, the promise is rejected. And the method is used with a promise to handle the error.
Working of JavaScript promise chaining
Example: loadScript
Let’s use this feature with the promisified , defined in the , to load scripts one by one, in sequence:
This code can be made bit shorter with arrow functions:
Here each call returns a promise, and the next runs when it resolves. Then it initiates the loading of the next script. So scripts are loaded one after another.
We can add more asynchronous actions to the chain. Please note that the code is still “flat” — it grows down, not to the right. There are no signs of the “pyramid of doom”.
Technically, we could add directly to each , like this:
This code does the same: loads 3 scripts in sequence. But it “grows to the right”. So we have the same problem as with callbacks.
People who start to use promises sometimes don’t know about chaining, so they write it this way. Generally, chaining is preferred.
Sometimes it’s ok to write directly, because the nested function has access to the outer scope. In the example above the most nested callback has access to all variables , , . But that’s an exception rather than a rule.
Thenables
To be precise, a handler may return not exactly a promise, but a so-called “thenable” object – an arbitrary object that has a method . It will be treated the same way as a promise.
The idea is that 3rd-party libraries may implement “promise-compatible” objects of their own. They can have an extended set of methods, but also be compatible with native promises, because they implement .
Here’s an example of a thenable object:
JavaScript checks the object returned by the handler in line : if it has a callable method named , then it calls that method providing native functions , as arguments (similar to an executor) and waits until one of them is called. In the example above is called after 1 second . Then the result is passed further down the chain.
This feature allows us to integrate custom objects with promise chains without having to inherit from .
Сложный кейс с промисами, и главное преимущество промисов — колбеки
Попалась мне интереснейшая задача «Платежная система отвечает об успешной оплате не сразу, поэтому придется слать несколько запросов в течение 30 секунд, при этом держать пользователя в режиме прелоадера, при этом если оплата пройдет раньше чем 30 секунд, то из цикла нужно выйти и отключить прелоадер, и если за 30 секунд ответа не получено, то показать ошибку».
Архитектура:
- Интервал запросов к серверу 1 секунда.
- Необходим один большой (глобальный) промис, чтобы было удобно отключить прелоадер.
- До входа в асинхронный код нужно включить прелоадер.
- Внутри асинхронного кода должно произойти «нечто ужасное» без потери читабельности.
- Отключение прелоадера должно происходить в финальном коде, независимо от того, успешная оплата, или ошибка, и независимо от того, сколько промисов будет использоваться в асинхронном коде.
Для удобства сопоставления алгоритма и архитектуры код совсем чуть-чуть упрощен, и совпадает с оригинальным на 90%.
Результат:
На данном примере видно как используются колбэки — у нас есть полный простор в передаче ошибок в родительский промис, их множественный вызов в разных местах, когда нам необходимо. И также максимальный простор для выбора момента уведомления «родителя» об успешном окончании, тем самым мы решаем 5 пункт из запланированной архитектуры.
Promise.any
Similar to , but waits only for the first fulfilled promise and gets its result. If all of the given promises are rejected, then the returned promise is rejected with – a special error object that stores all promise errors in its property.
The syntax is:
For instance, here the result will be :
The first promise here was fastest, but it was rejected, so the second promise became the result. After the first fulfilled promise “wins the race”, all further results are ignored.
Here’s an example when all promises fail:
As you can see, error objects for failed promises are available in the property of the object.
Итого
Мы ознакомились с пятью статическими методами класса :
- – ожидает выполнения всех промисов и возвращает массив с результатами. Если любой из указанных промисов вернёт ошибку, то результатом работы будет эта ошибка, результаты остальных промисов будут игнорироваться.
-
(добавлен недавно) – ждёт, пока все промисы завершатся и возвращает их результаты в виде массива с объектами, у каждого объекта два свойства:
- : , если выполнен успешно или , если ошибка,
- – результат, если успешно или – ошибка, если нет.
- – ожидает первый выполненный промис, который становится его результатом, остальные игнорируются.
- – возвращает успешно выполнившийся промис с результатом .
- – возвращает промис с ошибкой .
Из всех перечисленных методов, самый часто используемый – это, пожалуй, .