Promise javascript что это
Промисы
Представьте, что вы известный певец, которого фанаты постоянно донимают расспросами о предстоящем сингле.
Чтобы получить передышку, вы обещаете разослать им сингл, когда он будет выпущен. Вы даёте фанатам список, в который они могут записаться. Они могут оставить там свой e-mail, чтобы получить песню, как только она выйдет. И даже больше: если что-то пойдёт не так, например, в студии будет пожар и песню выпустить не выйдет, они также получат уведомление об этом.
Все счастливы! Вы счастливы, потому что вас больше не донимают фанаты, а фанаты могут больше не беспокоиться, что пропустят новый сингл.
Это аналогия из реальной жизни для ситуаций, с которыми мы часто сталкиваемся в программировании:
Аналогия не совсем точна, потому что объект Promise в JavaScript гораздо сложнее простого списка подписок: он обладает дополнительными возможностями и ограничениями. Но для начала и такая аналогия хороша.
Синтаксис создания Promise :
Её аргументы resolve и reject – это колбэки, которые предоставляет сам JavaScript. Наш код – только внутри исполнителя.
Когда он получает результат, сейчас или позже – не важно, он должен вызвать один из этих колбэков:
Так что исполнитель по итогу переводит promise в одно из двух состояний:
Позже мы рассмотрим, как «фанаты» узнают об этих изменениях.
Ниже пример конструктора Promise и простого исполнителя с кодом, дающим результат с задержкой (через setTimeout ):
Мы можем наблюдать две вещи, запустив код выше:
Это был пример успешно выполненной задачи, в результате мы получили «успешно выполненный» промис.
А теперь пример, в котором исполнитель сообщит, что задача выполнена с ошибкой:
Промис – и успешный, и отклонённый будем называть «завершённым», в отличие от изначального промиса «в ожидании».
Все последующие вызовы resolve и reject будут проигнорированы:
Идея в том, что задача, выполняемая исполнителем, может иметь только один итог: результат или ошибку.
Также заметим, что функция resolve / reject ожидает только один аргумент (или ни одного). Все дополнительные аргументы будут проигнорированы.
Это может случиться, например, когда мы начали выполнять какую-то задачу, но тут же увидели, что ранее её уже выполняли, и результат закеширован.
Потребители: then, catch, finally
Например, вот реакция на успешно выполненный промис:
Promise
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Более новая информация по этой теме находится на странице https://learn.javascript.ru/promise-basics.
Promise (обычно их так и называют «промисы») – предоставляют удобный способ организации асинхронного кода.
В современном JavaScript промисы часто используются в том числе и неявно, при помощи генераторов, но об этом чуть позже.
Что такое Promise?
Promise – это специальный объект, который содержит своё состояние. Вначале pending («ожидание»), затем – одно из: fulfilled («выполнено успешно») или rejected («выполнено с ошибкой»).
На promise можно навешивать колбэки двух типов:
Способ использования, в общих чертах, такой:
Синтаксис создания Promise :
Универсальный метод для навешивания обработчиков:
С его помощью можно назначить как оба обработчика сразу, так и только один:
Если в функции промиса происходит синхронный throw (или иная ошибка), то вызывается reject :
Посмотрим, как это выглядит вместе, на простом примере.
Пример с setTimeout
Возьмём setTimeout в качестве асинхронной операции, которая должна через некоторое время успешно завершиться с результатом «result»:
В результате запуска кода выше – через 1 секунду выведется «Fulfilled: result».
Функции resolve/reject принимают ровно один аргумент – результат/ошибку.
Promise после reject/resolve – неизменны
Заметим, что после вызова resolve/reject промис уже не может «передумать».
Когда промис переходит в состояние «выполнен» – с результатом (resolve) или ошибкой (reject) – это навсегда.
Последующие вызовы resolve/reject будут просто проигнорированы.
А так – наоборот, ошибка будет раньше:
Промисификация
Промисификация – это когда берут асинхронную функциональность и делают для неё обёртку, возвращающую промис.
После промисификации использование функциональности зачастую становится гораздо удобнее.
В качестве примера сделаем такую обёртку для запросов при помощи XMLHttpRequest.
Функция httpGet(url) будет возвращать промис, который при успешной загрузке данных с url будет переходить в fulfilled с этими данными, а при ошибке – в rejected с информацией об ошибке:
Цепочки промисов
«Чейнинг» (chaining), то есть возможность строить асинхронные цепочки из промисов – пожалуй, основная причина, из-за которой существуют и активно используются промисы.
Например, мы хотим по очереди:
Самое главное в этом коде – последовательность вызовов:
Если очередной then вернул промис, то далее по цепочке будет передан не сам этот промис, а его результат.
Схематично его работу можно изобразить так:
Значком «песочные часы» помечены периоды ожидания, которых всего два: в исходном httpGet и в подвызове далее по цепочке.
Если then возвращает промис, то до его выполнения может пройти некоторое время, оставшаяся часть цепочки будет ждать.
То есть, логика довольно проста:
Обратим внимание, что последний then в нашем примере ничего не возвращает. Если мы хотим, чтобы после setTimeout (*) асинхронная цепочка могла быть продолжена, то последний then тоже должен вернуть промис. Это общее правило: если внутри then стартует новый асинхронный процесс, то для того, чтобы оставшаяся часть цепочки выполнилась после его окончания, мы должны вернуть промис.
Строку (*) для этого нужно переписать так:
Перехват ошибок
Выше мы рассмотрели «идеальный случай» выполнения, когда ошибок нет.
А что, если github не отвечает? Или JSON.parse бросил синтаксическую ошибку при обработке данных?
Да мало ли, где ошибка…
Правило здесь очень простое.
Чтобы поймать всевозможные ошибки, которые возникнут при загрузке и обработке данных, добавим catch в конец нашей цепочки:
Есть два варианта развития событий:
Промисы в деталях
Самым основным источником информации по промисам является, разумеется, стандарт.
Чтобы наше понимание промисов было полным, и мы могли с лёгкостью разрешать сложные ситуации, посмотрим внимательнее, что такое промис и как он работает, но уже не в общих словах, а детально, в соответствии со стандартом ECMAScript.
Согласно стандарту, у объекта new Promise(executor) при создании есть четыре внутренних свойства:
Он делает следующее:
Здесь важно, что обработчики можно добавлять в любой момент. Можно до выполнения промиса (они подождут), а можно – после (выполнятся в ближайшее время, через асинхронную очередь).
Путеводитель по 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().
Когда вызываются данные методы, «обещание» устанавливается в свое конечное состояние «выполнено» или «отклонено», соответственно. После этого состояние изменить нельзя. Примеры можно посмотреть в комментарии.
Использование промисов
Например, вместо старомодной функции, которая принимает два колбэка и вызывает один из них в зависимости от успешного или неудачного завершения операции:
…современные функции возвращают промис, в который вы записываете ваши колбэки:
Мы называем это асинхронным вызовом функции. У этого соглашения есть несколько преимуществ. Давайте рассмотрим их.
Гарантии
В отличие от старомодных переданных колбэков промис даёт некоторые гарантии:
Цепочка вызовов
Вот в чём магия: функция then возвращает новый промис, отличающийся от первоначального:
По сути, каждый вызванный промис означает успешное завершение предыдущих шагов в цепочке.
Раньше выполнение нескольких асинхронных операций друг за другом приводило к классической «Вавилонской башне» колбэков:
Важно: Всегда возвращайте промисы в return, иначе колбэки не будут сцеплены и ошибки могут быть не пойманы (стрелочные функции неявно возвращают результат, если скобки <> вокруг тела функции опущены).
Цепочка вызовов после catch
В результате выведется данный текст:
Заметьте, что текст «Выведи это» не вывелся, потому что «Где то произошла ошибка» привела к отказу
Распространение ошибки
Вы могли ранее заметить, что failureCallback повторяется три раза в «pyramid of doom», а в цепочке промисов всего лишь один раз:
В основном, цепочка промисов останавливает выполнение кода, если где-либо произошла ошибка, и вместо этого ищет далее по цепочке обработчики ошибок. Это очень похоже на то, как работает синхронный код:
Эта симметрия с синхронным кодом лучше всего показывает себя в синтаксическом сахаре async / await в ECMAScript 2017:
Промисы решают основную проблему пирамид, обработку всех ошибок, даже вызовов исключений и программных ошибок. Это основа для функционального построения асинхронных операций.
Создание промиса вокруг старого колбэка
Promise может быть создан с помощью конструктора. Это может понадобится только для старых API.
В идеале, все асинхронные функции уже должны возвращать промис. Но увы, некоторые APIs до сих пор ожидают успешного или неудачного колбэка переданных по старинке. Типичный пример: setTimeout() функция:
Смешивание старого колбэк-стиля и промисов проблематично. В случае неудачного завершения saySomething или программной ошибки, нельзя обработать ошибку.
К с частью мы можем обернуть функцию в промис. Хороший тон оборачивать проблематичные функции на самом низком возможном уровне, и больше никогда их не вызывать на прямую:
В сущности, конструктор промиса становится исполнителем функции, который позволяет нам резолвить или режектить промис вручную. Так как setTimeout всегда успешен, мы опустили reject в этом случае.
Композиция
Promise.resolve() и Promise.reject() короткий способ создать уже успешные или отклонённые промисы соответственно. Это иногда бывает полезно.
Последовательное выполнение композиции возможно при помощи хитрости JavaScript:
Фактически, мы превращаем массив асинхронных функций в цепочку промисов равносильно: Promise.resolve().then(func1).then(func2);
Это также можно сделать, объединив композицию в функцию, в функциональном стиле программирования:
composeAsync функция примет любое количество функций в качестве аргументов и вернёт новую функцию которая примет в параметрах начальное значение, переданное по цепочке. Это удобно, потому что некоторые или все функции могут быть либо асинхронными либо синхронными, и они гарантированно выполнятся в правильной последовательности:
В ECMAScript 2017, последовательные композиции могут быть выполнены более простым способом с помощью async/await:
Порядок выполнения
Чтобы избежать сюрпризов, функции, переданные в then никогда не будут вызваны синхронно, даже с уже разрешённым промисом:
Вместо немедленного выполнения, переданная функция встанет в очередь микрозадач, а значит выполнится, когда очередь будет пустой в конце текущего вызова JavaScript цикла событий (event loop), т.е. очень скоро:
Вложенность
Простые цепочки promise лучше оставлять без вложений, так как вложенность может быть результатом небрежной структуры. Смотрите распространённые ошибки.
Обратите внимание, что необязательный шаги здесь выделены отступом.
Внутренний оператор catch нейтрализует и перехватывает ошибки только от doSomethingOptional() и doSomethingExtraNice(), после чего код возобновляется с помощью moreCriticalStuff(). Важно, что в случае сбоя doSomethingCritical() его ошибка перехватывается только последним (внешним) catch.
Частые ошибки
В этом разделе собраны частые ошибки, возникающие при создании цепочек промисов. Несколько таких ошибок можно увидеть в следующем примере:
Вторая ошибка это излишняя вложенность, включая первую ошибку. Вложенность также ограничивает область видимости внутренних обработчиков ошибок, если это не то чего хотел разработчик, это может привести к необработанным ошибкам. Примером этого является пример как не нужно создавать промисы, который комбинирует вложенность с чрезмерным использованием конструктора промисов для оборачивания кода который уже использует промисы.
Хорошим примером является всегда либо возвращать либо заканчивать цепочки промисов, и как только вы получаете новый промис, возвращайте его сразу же, чтобы не усложнять код излишней вложенностью:
Обратите внимание что () => x это сокращённая форма () => < return x; >.
Теперь у нас имеется единственная определённая цепочка с правильной обработкой ошибок.
Знакомство с промисами в JavaScript
Jan 15, 2020 · 4 min read
Если вы не совсем в курсе современных тенденций JavaScript, то, по крайней мере, слышали о промисах ранее, но не знаете, где и как их можно было бы применить.
Промисы служат для управления асинхронными операциями в JavaScript. Они достаточно легки в применении и могут избавить вас от многих неприятностей. Когда у вас имеется множество асинхронных операций, промисы могут избавить от лишней мороки с формированием огромного числа обратных вызовов, которые могут перерасти в кошмар и сделать код неуправляемым.
Проблема обратных вызовов
Хотя промисы и существую т уже на протяжении нескольких лет, давайте все же вспомним времена, когда их не было. Это поможет лучше понять, почему они являются хорошим способом управления асинхронными операциями.
Изначально для этого применялись функции обратного вызова. Они были наиболее удобными в этом отношении, но все равно имели ряд недостатков. Давайте взглянем на такой пример:
Как вы видите, функция loadScript передается параметром обратного вызова, который выполняется после загрузки скрипта. Все это работает прекрасно, но возникает один большой вопрос: “Что случится, если скрипт по какой-либо причине не загрузится?”. Со стороны разработчика будет наивным игнорировать такую возможность.
Как же перестраховаться? Достаточно просто. Мы добавляем еще один параметр в функцию обратного вызова, который сообщает об отсутствии ошибок при загрузке скрипта.
Но что, если у нас будет ситуация, в которой мы будем зависеть от загрузки не одного изображения, а, например, трех или более?
Именно такие ситуации и приводят к возникновению кошмара в обратных вызовах.
Краткое введение
Прежде чем приступить к рассмотрению промисов и принципов их работы, давайте взглянем, что на эту тему говорит MDN:
“Объект Promise представляет окончательное выполнение (или провал) асинхронной операции, а также ее итоговое значение.”
Как отмечено в MDN, промисы применяются для управления асинхронными операциями. Фактически же они позволяют не только легко работать с множественными операциями, но и более эффективно обрабатывать ошибки, что гораздо сложнее делать при помощи обратных вызовов и событий. В дополнение к этому они еще и повышают читаемость кода.
Промис может иметь одно из четырех состояний:
Промисы в коде
Теперь давайте создадим простой пример промиса для лучшего понимания:
В этом примере мы пробуем запросить данные из API. Если API вернет HTTP код состояния 200, значит запрос оказался успешным. В противном случае он провалился.
Как же нам применять промис, который мы только что создали?
All и Race
Идеальным примером применения Promise.all может выступить одновременная множественная отправка AJAX запросов.
Вместо ожидания завершения всех промисов, Promise.race запускается, как только оказывается выполнен или отклонен хотя бы один промис в цепочке.