Promise
Сводка
Объект Promise используется для отложенных и асинхронных вычислений.
Синтаксис
Параметры
Описание
Интерфейс Promise (промис) представляет собой обёртку для значения, неизвестного на момент создания промиса. Он позволяет обрабатывать результаты асинхронных операций так, как если бы они были синхронными: вместо конечного результата асинхронного метода возвращается своего рода обещание (дословный перевод слова «промис») получить результат в некоторый момент в будущем.
Promise может находиться в трёх состояниях:
Так как методы Promise.prototype.then() и Promise.prototype.catch() сами возвращают промис, их можно вызывать цепочкой, создавая соединения.
Примечание: говорят, что промис находится в состоянии завершён (settled) когда он или исполнен или отклонён, т.е. в любом состоянии, кроме ожидания (это лишь форма речи, не являющаяся настоящим состоянием промиса). Также можно встретить термин исполнен (resolved) — это значит что промис завершён или «заблокирован» в ожидании завершения другого промиса. В статье состояния и fates приводится более подробное описание терминологии.
Свойства
Методы
Создание промиса
Объект Promise создаётся при помощи ключевого слова new и своего конструктора. Конструктор Promise принимает в качестве аргумента функцию, называемую «исполнитель» (executor function). Эта функция должна принимать две функции-колбэка в качестве параметров. Первый из них ( resolve ) вызывается, когда асинхронная операция завершилась успешно и вернула результат своего исполнения в виде значения. Второй колбэк ( reject ) вызывается, когда операция не удалась, и возвращает значение, указывающее на причину неудачи, чаще всего объект ошибки.
Чтобы снабдить функцию функциональностью промисов, нужно просто вернуть в ней объект Promise :
Путеводитель по JavaScript Promise для новичков
Этот материал мы подготовили для JavaScript-программистов, которые только начинают разбираться с «Promise». Обещания (promises) в JavaScript – это новый инструмент для работы с отложенными или асинхронными вычислениями, добавленный в ECMAScript 2015 (6-я версия ECMA-262).
До появления «обещаний» асинхронные задачи можно было решать с помощью функций обратного вызова или с помощью обработки событий. Универсальный подход к решению асинхронных задач – обработка событий. Менее удобный, но также имеющий право на существование, способ использовать функции обратного вызова. Конечно, выбор решения зависит от стоящей перед вами задачи. Вариант решения задач с помощью «обещаний», скорее, призван заменить подход к функциями обратного вызова.
В использовании функций обратного вызова есть существенный недостаток с точки зрения организации кода: «callback hell«. Этот недостаток заключается в том, что в функции обратного вызова есть параметр, который, в свою очередь, также является функцией обратного вызова – и так может продолжаться до бесконечности.
Может образоваться несколько уровней таких вложенностей. Это приводит к плохому чтению кода и запутанности между вызовами функций обратного вызова. Это, в свою очередь, приведет к ошибкам. С такой структурой кода найти ошибки очень сложно.
Если все же использовать такой подход, то более эффективно будет инициализировать функции обратного вызова отдельно, создавая их в нужном месте.
Давайте рассмотрим работу «обещаний» на примере конкретной задачи:
После загрузки страницы браузера необходимо показать изображения из указанного списка.
Список представляет собой массив, в котором указан путь к изображению. Например, для показа изображений в слайдере вашей баннерной системы на сайте или асинхронной загрузки изображений в фотоальбоме.
Сначала напишем функцию, которая подгружает одно изображение по указанному url.
Объект «обещание» создается с помощью конструктора new Promise(. ), которому в качестве аргумента передается анонимная функция с двумя параметрами: resolve, reject. Они, в свою очередь, так же являются функциями. Resolve() — сообщает о том, что код выполнен «успешно», reject() – код выполнен с «ошибкой» (что считать «ошибкой» при выполнении вашего кода, решать вам. Это что-то вроде if(true) <. >else <. >).
Интерфейс Promise (обещание) представляет собой обертку для значения, неизвестного на момент создания обещания. Он позволяет обрабатывать результаты асинхронных операций так, как если бы они были синхронными: вместо конечного результата асинхронного метода возвращается обещание, результат которого можно получить в некоторый момент в будущем.
При создании обещание находится в ожидании (состояние pending), а затем может стать выполнено (fulfilled), вернув полученный результат (значение), или отклонено (rejected), вернув причину отказа.
В методы resolve() и reject() можно передавать любые объекты. В метод reject(), как правило, передают объект типа Error с указанием причины ошибки («отклоненного» состояния «обещания»). В любом случае, это не обязательно. Решение, как дальше вы будете обрабатывать такие ситуации – за вами.
На данный момент может показаться, что «обещание» совершенно не нужно использовать в этой ситуации. Пока мы лишь устанавливаем некий индикатор того, было ли подгружено изображение. Однако вскоре вы увидите, что этот механизм может легко, интуитивно понятно определять, что произойдет после того, как задача будет выполнена (изображение подгружено или нет).
Методы then() и catch()
Всякий раз, когда вы создаете объект «обещание», становятся доступны два метода: then() и catch(). Используя их, вы можете выполнить нужный код при успешном разрешении «обещания» (resolve(. )) или же код, обрабатывающий ситуацию с «ошибкой» (reject(. )).
Примечание: не обязательно возвращать (return) resolve(. ) или reject(. ):. В примере выше можно было бы написать так:
В результате вызова myPromise() все равно сработал бы метод then() или catch(). Лучше всего завести сразу привычку — всегда возвращать resolve(. ) или reject(. ). В будущем это поможет избежать ситуации, когда код будет работать не так, как ожидается.
В методы then() и catch() передают две анонимные функции. Синтаксис метода then() в общем случае такой:
Параметр function onSuccess()<> будет вызван в случае успешного выполнения «обещания», function onFail()<> – в случае ошибки. По этой причине следующий код будет работать одинаково:
Гораздо привычнее и понятнее использовать catch(. ). Также метод catch() можно вызывать «посередине» цепочки вызовов then(), если логика вашего кода того требует: then().catch().then().Не забывайте вызывать catch() последним в цепочке: это позволит вам всегда отлавливать «ошибочные» ситуации.
Вызовем наш метод loadImage(url) и для примера добавим одну картинку на страницу:
Последовательная рекурсивная подгрузка и отображение изображений
Напишем функцию для последовательного отображения изображений:
Функция displayImages(images) последовательно проходит по массиву с url изображений. В случае успешной подгрузки мы добавляем изображение на страницу и переходим к следующему url в списке. В противоположном случае – просто переходим к следующему url в списке.
Возможно, такое поведение отображения изображений не совсем то, что необходимо в данном случае. Если требуется показать все изображение только после того, как они были загружены, нужно реализовать работу с массивом «обещаний».
В массиве promiseImgs теперь находятся «обещания», у которых состояние может быть как «разрешено» так и «отклонено», так как изображения fake.jpg физически не существует.
Для завершения задачи можно было бы воспользоваться методом Promise.all(. ).
Promise.all(iterable) возвращает обещание, которое выполнится после выполнения всех обещаний в передаваемом итерируемом аргументе.
Однако у нас в списке есть изображение, которого физически не существует. Поэтому методом Promise.all воспользоваться нельзя: нам необходимо проверять состояние объекта «обещание» (resolved | rejected).
Если в массиве «обещаний» есть хотя бы одно, которое «отклонено» (rejected), то метод Promise.all так же вернет «обещание» с таким состоянием, не дожидаясь прохождения по всему массиву.
Поэтому напишем функцию loadAndDisplayImages.
Подгружаем изображения, и показываем их на странице все сразу
Можно посмотреть сетевую активность в браузере и убедиться в параллельной работе (для наглядности в Chrome была включена эмуляция подключения по Wi-Fi (2ms, 30Mb/s, 15M/s):
Разобравшись, как работать с Promise, вам будет проще понять принципы работы, например, с API Яндекс.Карт, или Service Worker – именно там они используются.
UPD: В статье не озвучил один важный момент, с которым, отчасти, был связан совет писать return resolve() или return reject().
Когда вызываются данные методы, «обещание» устанавливается в свое конечное состояние «выполнено» или «отклонено», соответственно. После этого состояние изменить нельзя. Примеры можно посмотреть в комментарии.
Использование промисов в JavaScript
Периодически мы публикуем материалы, которые так или иначе касаются использования промисов в JavaScript.
Почему к промисам приковано столько внимания? Полагаем, всё дело в том, что технология эта весьма востребована, и в том, что в ней достаточно сложно разобраться.
Поэтому, если вы хотите лучше понять промисы, мы предлагаем вашему вниманию перевод очередной статьи, посвящённой этой теме. Её автор говорит, что он последние 10 лет занимался разработкой на Java и PHP, но всё это время с интересом поглядывал на JavaScript. Недавно он решил всерьёз заняться JS и первой заинтересовавшей его темой стали промисы.
Мы считаем, что этот материал будет интересен начинающим разработчикам, которые чувствуют, что, хотя и пользуются промисами, пока недостаточно хорошо их понимают. Вполне возможно, что рассказ того, кто смотрит на JavaScript свежим взглядом и стремится объяснить другим то, что понял сам, не считая, что какие-то вещи понятны всем и без объяснений, поможет начинающим в деле освоения механизмов JavaScript.
JavaScript глазами новичка
Тот, кто начинает писать на JavaScript, может почувствовать себя, что называется, «не в своей тарелке». Одни говорят, что JS — это синхронный язык программирования, другие утверждают, что он — асинхронный. Новичок слышит о коде, который блокирует главный поток, и о коде, который его не блокирует, о паттернах проектирования, основанных на событиях, о жизненном цикле событий, о стеке вызовов функций, об очереди событий и об их всплытии, о полифиллах. Он узнаёт, что существуют такие штуки, как Babel, Angular, React, Vue и мириады других библиотек. Если вы только что узнали в таком вот «новичке» себя — не стоит из-за этого волноваться. Вы — не первый и не последний. Есть даже термин для этого — так называемая «JavaScript-усталость» (JavaScript fatigue). По этому поводу метко высказался Лукас Ф Коста: «JavaScript-усталость — это то, что можно наблюдать тогда, когда люди используют инструменты, которые им не нужны, для решения проблем, которых у них нет».
Но не будем о грустном. Итак, JavaScript — это синхронный язык программирования, который, благодаря механизму коллбэков, позволяет вызывать функции так, как это делается в асинхронных языках.
Простой рассказ о промисах
Слово «promise» переводится как «обещание». Promise-объекты в программировании, которые мы называем «промисами», очень похожи на обычные обещания, которые люди дают друг другу в реальной жизни. Поэтому давайте сначала поговорим о таких вот обещаниях.
В Википедии можно найти следующее определение слова «обещание»: «обязательство, согласие кого-либо что-либо выполнить или, напротив, не делать». В словаре Ожегова «обещание» — это «добровольное обязательство сделать что-нибудь».
Итак, что мы знаем об обещаниях?
Промисы в JavaScript
У меня есть одно правило: занимаясь JavaScript, я всегда читаю документацию, которую можно найти на MDN. Мне кажется, что этот ресурс выгодно отличается от остальных конкретностью и чёткостью изложения. Поэтому, изучая промисы, я ознакомился с соответствующим материалом и поэкспериментировал с кодом для того, чтобы привыкнуть к новым для себя синтаксическим конструкциям.
Для того чтобы понять промисы, надо разобраться с двумя основными вещами. Первая — это создание промисов. Вторая — обработка результатов, возвращаемых промисами. Хотя большая часть кода, который мы пишем, направлена на работу с промисами, создаваемыми, например, некими библиотеками, полное понимание механизмов работы промисов, без сомнения, окажется полезным. Кроме того, программисту, который уже имеет некоторый опыт, знать о том, как создавать промисы, так же важно, как знать о том, как с ними работать.
Создание промисов
Вот как создают промисы:
Теперь, когда мы в общих чертах знаем, как создавать промисы, создадим простой промис для того, чтобы лучше во всём разобраться.
Вот что выведет этот код:
У промиса есть состояние (PromiseStatus) и значение (PromiseValue)
В этом коде создаётся промис, который безусловно разрешится через 10 секунд. Это даёт нам возможность взглянуть на состояние неразрешённого промиса.
Промис, разрешённый через 10 секунд и возвращающий объект
Взглянем теперь на промис, который мы решили не разрешить, а отклонить. Для этого модифицируем тот код, который уже использовался в первом примере.
Так как мы не обрабатываем ситуацию отклонения промиса, в консоли браузера (тут используется Google Chrome), будет выведено сообщение об ошибке. Подробнее мы поговорим об этом ниже.
Объект Promise
В соответствии с документацией MDN, объект Promise представляет собой результат успешного или неудачного завершения асинхронной операции.
▍Методы прототипа объекта Promise
Схема работы промиса (изображение взято отсюда)
Вот небольшая история. Вы — школьник и просите, чтобы ваша мама купила вам мобильник. Она говорит: «Если наши сбережения будут больше, чем стоит телефон, я его тебе куплю». Теперь перескажем эту историю языком JavaScript.
Вот что выведет этот код:
Мама не сдержала обещание
Если же мы изменим значение переменной momsSavings на 200000, тогда мама сможет купить сыну подарок. В таком случае вышеприведённый код выведет следующее.
Мама выполнила обещание
Выполните этот код в консоли браузера для того, чтобы увидеть, как ведут себя разрешённые и отклонённые промисы. Далее мы поговорим о том, как можно создавать множество промисов и проверять результаты их выполнения, пользуясь другими механизмами.
▍Статические методы объекта Promise
Надо отметить, что промис может иметь несколько обработчиков. Например, на основе предыдущего примера можно получить код, показанный ниже.
Вот что он выведет в консоль браузера:
▍Метод Promise.all
Пример №1
Здесь будут разрешены все промисы. Такой сценарий встречается чаще всего.
Вот что этот код выведет в консоль:
Все промисы разрешены
Проанализировав результаты выполнения этого примера, можно сделать два важных наблюдения.
Во-первых, третий промис, на разрешение которого нужно 2 секунды, завершается до завершения второго, но, как можно видеть из вывода, формируемого кодом, порядок промисов в массиве сохраняется.
Если бы промисы выполнялись последовательно, то время выполнения этой инструкции составило бы 7 секунд (1+4+2). Однако таймер сообщает нам о том, что вся эта операция заняла, если округлить результат, 4 секунды. Это является доказательством того, что все промисы выполняются параллельно.
Пример№ 2
Вот какой вывод сформирует этот код:
Вызов Promise.all с передачей этому методу массива, не содержащего промисов
Так как в массиве нет промисов, Promise.all почти мгновенно разрешается.
Пример №3
Как видно из результатов выполнения кода, показанных ниже, выполнение Promise.all останавливается после первого отклонённого промиса с выводом сообщения, которое выдаёт этот промис.
Выполнение останавливается после первого отклонённого промиса
▍Метод Promise.race
MDN сообщает, что метод Promise.race(iterable) возвращает разрешённый или отклонённый промис со значением или причиной отклонения, после того как один из переданных промисов будет, соответственно, разрешён или отклонён.
Пример №1
Вот что попадает в консоль после выполнения этого примера.
Промис, который разрешился быстрее всех остальных
Пример №2
В консоль, после выполнения этого примера, попадёт следующее:
Промис, отклонённый раньше всех
Общий пример и эксперименты
Я собрал все примеры, которые мы рассматривали в этом материале, в одном месте, что позволит удобнее с ними экспериментировать, исследовать различные сценарии работы с промисами. Этот код рассчитан на выполнение в браузере, поэтому тут мы не используем вызовов каких-либо API, не обращаемся к файловым операциям, не работаем с базами данных. Хотя всё это находит применение при разработке реальных проектов, полагаю, работа с этими механизмами может отвлечь нас от нашей главной цели — промисов. А использование простых функций, имитирующих, временные задержки, даёт похожие результаты и не обременяет нас дополнительными деталями.
Итоги
Предлагаю вам список правил, которых я придерживаюсь при работе с промисами для того, чтобы пользоваться ими правильно.
Уважаемые читатели! Скажите, испытывали ли вы сложности с освоением промисов, когда впервые с ними столкнулись?
Промо-код на скидку в 10% на наши виртуальные сервера:
Промисы в JavaScript для чайников
Промисы в JavaScript на самом деле совсем не сложные. Тем не менее, многие люди находят затруднительным их понимание с первых моментов изучения. Поэтому, снизу всё будет описано способом для начинающих, чтобы читающий понял материал полностью и объемно. Помните, что промисы лежат в основе async/await и их понимание, можно сказать, что обязательно для работы с асинхронным JavaScript.
Понимание промисов
Итак, вкратце про промисы: “Представьте, что вы ребенок. Ваша мама обещает вам, что вы получите новый телефон на следующей неделе.”
Вы не знаете, получите ли вы его до следующей неделе. Ваша мама может купить вам совершенно новый телефон, а может просто этого не сделать, к примеру, потому что она будет не в настроении 🙁
Это и есть промис. От английского promise — обещать. Небольшое уточнение, пожалуйста, не усложняйте понимание другим с произношением, так как во всём русскоязычном мире принято говорить “промис” в случае с JavaScript.
Итак, у промиса есть 3 состояния. Это:
1. Промис в состоянии ожидания ( pending ). Когда вы не знаете, получите ли вы мобильный телефон к следующей неделе или нет.
2. Промис решен ( resolved ). Вам реально купят новый телефон.
3. Промис отклонен ( rejected ). Вы не получили новый мобильный телефон, так как всё-таки, мама была не в настроении.
Создание промиса
Давайте переведем все это в JavaScript.
Код выше довольно выразителен и говорит сам за себя.
3. Тут у нас стандартный синтаксис для определения нового промиса, как в MDN документации. То есть синтаксис промиса выглядит таким образом.
Применяем промисы
Теперь у нас есть промис, давайте применим его:
Давайте запустим этот пример и увидим результат!
Цепочки промисов
Да, в промисах есть цепочки.
Давайте представим, что вы ребенок и обещали своему другу, что покажете ему новый телефон, когда вам его купят. Это будет ещё один промис.
Вот так легко связывать промисы.
Промисы и асинхронность
Промисы асинхронны. Давайте выведем сообщение перед и после вызовом промиса.
Какова последовательность ожидаемого вывода? Возможно вы ожидали.
Но на самом деле вывод будет таким:
Почему? Потому что жизнь (или JS) никого не ждёт.
Промисы в ES5, ES6/2015, ES7/Next
ES 5 — поддерживают почти все браузеры. Демо код работает в ES5 среде (Все основные браузеры + NodeJS), если бы вы подключили библиотеку промисов Bluebird. Почему так? Потому что ES5 не поддерживает промисы из коробки. Другая знаменитая библиотека промисов это Q, от Криса Коваля.
1. Всегда, когда вам нужно возвратить промис в функцию, вы ставите спереди async к этой функции. Для примера, async function showOff(phone)
3. Используйте try < … >catch(error) < … >, чтобы словить ошибку промиса, отклоненную промисом.
Почему промисы и когда их использовать?
Зачем они нам нужны? Как выглядел мир до промисов? Перед ответом на эти вопросы, давайте вернемся к основам.
Нормальная функция против асинхронной.
Давайте посмотрим на эти два примера, каждый пример делает сложение двух чисел, один пример с нормальной функцией, другой с удаленной.
Нормальная функция для сложения чисел.
Асинхронная функция для сложения двух чисел:
Если вы сложите числа нормальной функцией, то вы сразу же получите результат. Тем не менее, если в вашем случае нужен удаленный запрос для получения результата, то вам нужно подождать, тут вы не сможете получить результат мгновенно.
Или таким способом вы вообще не можете знать — получите ли вы результат, потому что сервер может просто упасть, тормознуть с ответом и т. п. Вам не нужно, чтобы весь процесс был заблокирован, пока вы ждете результат.
Вызов API, скачивание файлов, чтение файлов — всё это те обычные async операции, которые вы можете выполнять.
Мир до промисов — колбэки.
Должны ли мы использовать промисы для каждого асинхронного запроса? Нет. До промисов, мы используем колбэки. Колбэки это просто функция, которую вы вызываете, когда получаете отдаваемый результат. Давайте модифицируем предыдущий пример, чтобы разрешить колбэк.
Синтаксис ок, зачем нам тогда промисы?
Что если вы захотите сделать последующее асинхронное действие?
Давайте представим, что вместо простого сложения чисел единожды, нам надо будет сделать это 3 раза. В обычной функции, мы делаем это:
Как это выглядит с колбэками?
Этот синтаксис менее дружелюбен. Он выглядит как пирамида, но люди обычно называют подобное «колбэк адом», потому что колбэки, вложенные в колбэки, кхм, представьте, что у вас 10 колбэков и ваш код будет вложен 10 раз.
Побег из колбэк ада
Промисы приходят на помощь. Давайте посмотрим на тот же код, но по версии промисов.
Новичок на районе: Observables
Перед тем как закончить с промисами, есть кое-что, что пришло для того, чтобы облегчить работу с асинхронными данными — это Observables.
Observables — это ленивые потоки событий, которые могут выдать ноль или больше событий, а могут и вообще не закончиться.
Некоторые ключевые различия между промисами и observables:
Observable.fromPromise конвертит промис в observable поток
Observables могут делать много забавных вещей довольно легко. Для примера, delay добавляет функцию за 3 секунды с всего-лишь одной строкой кода или пробовать заново, так что вы можете делать запрос определенное количество раз.
Заключение
Узнайте про колбэки и промисы. Поймите их и используйте их. Не беспокойтесь о Observables, пока что. Все трое могут повлиять процесс разработки, но всё это будет зависеть от ситуации.








