Что такое promise js
Перейти к содержимому

Что такое promise js

  • автор:

Promise

Объект Promise используется для отложенных и асинхронных вычислений.

Интерактивный пример

Синтаксис

new Promise(executor); new Promise(function(resolve, reject)  . >); 

Параметры

Объект функции с двумя аргументами resolve и reject . Функция executor получает оба аргумента и выполняется сразу, ещё до того как конструктор вернёт созданный объект. Первый аргумент ( resolve ) вызывает успешное исполнение промиса, второй ( reject ) отклоняет его. Обычно функция executor описывает выполнение какой-то асинхронной работы, по завершении которой необходимо вызвать функцию resolve или reject . Обратите внимание, что возвращаемое значение функции executor игнорируется.

Описание

Интерфейс Promise (промис) представляет собой обёртку для значения, неизвестного на момент создания промиса. Он позволяет обрабатывать результаты асинхронных операций так, как если бы они были синхронными: вместо конечного результата асинхронного метода возвращается своего рода обещание (дословный перевод слова «промис») получить результат в некоторый момент в будущем.

Promise может находиться в трёх состояниях:

  • ожидание (pending): начальное состояние, не исполнен и не отклонён.
  • исполнено (fulfilled): операция завершена успешно.
  • отклонено (rejected): операция завершена с ошибкой.

При создании промис находится в ожидании (pending), а затем может стать исполненным (fulfilled), вернув полученный результат (значение), или отклонённым (rejected), вернув причину отказа. В любом из этих случаев вызывается обработчик, прикреплённый к промису методом then . (Если в момент назначения обработчика промис уже исполнен или отклонён, обработчик всё равно будет вызван, т.е. асинхронное исполнение промиса и назначение обработчика не будет происходить в «состоянии гонки», как, например, в случае с событиями в DOM.)

Так как методы Promise.prototype.then() и Promise.prototype.catch() сами возвращают промис, их можно вызывать цепочкой, создавая соединения.

Примечание: говорят, что промис находится в состоянии завершён (settled) когда он или исполнен или отклонён, т.е. в любом состоянии, кроме ожидания (это лишь форма речи, не являющаяся настоящим состоянием промиса). Также можно встретить термин исполнен (resolved) — это значит что промис завершён или «заблокирован» в ожидании завершения другого промиса. В статье состояния и fates приводится более подробное описание терминологии.

Свойства

Значение свойства всегда равно 1 (количество аргументов конструктора).

Представляет прототип для конструктора Promise .

Методы

Ожидает исполнения всех промисов или отклонения любого из них.

Возвращает промис, который исполнится после исполнения всех промисов в iterable . В случае, если любой из промисов будет отклонён, Promise.all будет также отклонён.

Ожидает завершения всех полученных промисов (как исполнения так и отклонения).

Возвращает промис, который исполняется когда все полученные промисы завершены (исполнены или отклонены), содержащий массив результатов исполнения полученных промисов.

Ожидает исполнения или отклонения любого из полученных промисов.

Возвращает промис, который будет исполнен или отклонён с результатом исполнения первого исполненного или отклонённого промиса из iterable .

Возвращает промис, отклонённый из-за reason .

Возвращает промис, исполненный с результатом value .

Создание промиса

Объект Promise создаётся при помощи ключевого слова new и своего конструктора. Конструктор Promise принимает в качестве аргумента функцию, называемую «исполнитель» (executor function). Эта функция должна принимать две функции-колбэка в качестве параметров. Первый из них ( resolve ) вызывается, когда асинхронная операция завершилась успешно и вернула результат своего исполнения в виде значения. Второй колбэк ( reject ) вызывается, когда операция не удалась, и возвращает значение, указывающее на причину неудачи, чаще всего объект ошибки.

const myFirstPromise = new Promise((resolve, reject) =>  // выполняется асинхронная операция, которая в итоге вызовет: // // resolve(someValue); // успешное завершение // или // reject("failure reason"); // неудача >); 

Чтобы снабдить функцию функциональностью промисов, нужно просто вернуть в ней объект Promise :

function myAsyncFunction(url)  return new Promise((resolve, reject) =>  const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.onload = () => resolve(xhr.responseText); xhr.onerror = () => reject(xhr.statusText); xhr.send(); >); > 

Примеры

Простой пример

js
let myFirstPromise = new Promise((resolve, reject) =>  // Мы вызываем resolve(. ), когда асинхронная операция завершилась успешно, и reject(. ), когда она не удалась. // В этом примере мы используем setTimeout(. ), чтобы симулировать асинхронный код. // В реальности вы, скорее всего, будете использовать XHR, HTML5 API или что-то подобное. setTimeout(function ()  resolve("Success!"); // Ура! Всё прошло хорошо! >, 250); >); myFirstPromise.then((successMessage) =>  // successMessage - это что угодно, что мы передали в функцию resolve(. ) выше. // Это необязательно строка, но если это всего лишь сообщение об успешном завершении, это наверняка будет она. console.log("Ура! " + successMessage); >); 

Продвинутый пример

html
button id="btn">Создать Promise!button> div id="log">div> 

исполнение промиса протоколируется при помощи продолжения p1.then . Это показывает как синхронная часть метода отвязана от асинхронного завершения промиса.

var promiseCount = 0; function testPromise()  var thisPromiseCount = ++promiseCount; var log = document.getElementById('log'); log.insertAdjacentHTML('beforeend', thisPromiseCount + ') Запуск (запуск синхронного кода) '); // Создаём промис, возвращающее 'result' (по истечении 3-х секунд) var p1 = new Promise( // Функция разрешения позволяет завершить успешно или // отклонить промис function(resolve, reject)  log.insertAdjacentHTML('beforeend', thisPromiseCount + ') Запуск промиса (запуск асинхронного кода) '); // Это всего лишь пример асинхронности window.setTimeout( function()  // Промис исполнен! resolve(thisPromiseCount) >, Math.random() * 2000 + 1000); >); // Указываем, что сделать с исполненным промисом p1.then( // Записываем в протокол function(val)  log.insertAdjacentHTML('beforeend', val + ') Промис исполнен (асинхронный код завершён) '); >); log.insertAdjacentHTML('beforeend', thisPromiseCount + ') Промис создан (синхронный код завершён) '); > 
if ("Promise" in window)  btn = document.getElementById("btn"); btn.addEventListener("click", testPromise); > else  log = document.getElementById("log"); log.innerHTML = "Live example not available as your browser doesn't support the Promise interface."; > 
if ("Promise" in window)  let btn = document.getElementById("btn"); btn.addEventListener("click", testPromise); > else  log = document.getElementById("log"); log.innerHTML = "Демонстрация невозможна, поскольку ваш браузер не поддерживает интерфейс Promise."; > 

Данный пример запускается при нажатии на кнопку. Для этого вам необходим браузер, поддерживающий Promise . При последовательных нажатиях на кнопку с коротким интервалом, вы можете увидеть как различные промиса будут исполнены один за другим.

Загрузка изображения при помощи XHR

Другой простой пример использования Promise и XMLHttpRequest для загрузки изображения доступен в репозитории MDNpromise-test на GitHub. Вы также можете посмотреть его в действии. Каждый шаг прокомментирован и вы можете подробно исследовать Promise и XHR.

Спецификации

Specification
ECMAScript Language Specification
# sec-promise-objects

Совместимость с браузерами

BCD tables only load in the browser

Смотрите также

  • Спецификация Promises/A+
  • Jake Archibald: JavaScript Promises: There and Back Again
  • Domenic Denicola: Callbacks, Promises, and Coroutines – Asynchronous Programming Pattern in JavaScript
  • Matt Greer: JavaScript Promises . In Wicked Detail
  • Forbes Lindesay: promisejs.org
  • Nolan Lawson: We have a problem with promises — Common mistakes with promises
  • Promise polyfill
  • Udacity: JavaScript Promises

Found a content problem with this page?

  • Edit the page on GitHub.
  • Report the content issue.
  • View the source on GitHub.

This page was last modified on 7 авг. 2023 г. by MDN contributors.

Your blueprint for a better internet.

Promise

Как уйти за значением выражения и вернуться, когда оно будет доступно.

Время чтения: 11 мин

Открыть/закрыть навигацию по статье

  1. Кратко
  2. Как пишется
  3. Как понять
  4. Методы
    1. Цепочки методов
    2. Обработка ошибок в цепочках методов
    1. Николай Лопин советует
    1. Реализуйте полифил для Promise.all

    Контрибьюторы:

    Обновлено 29 декабря 2023

    Эта документация связана с понятием асинхронности в JavaScript. Зачем нужен асинхронный код и как он работает описано в обзорной статье «Асинхронность в JS».

    Кратко

    Скопировать ссылку "Кратко" Скопировано

    Промис (Promise) — специальный объект JavaScript, который используется для написания и обработки асинхронного кода.

    Асинхронные функции возвращают объект Promise в качестве значения. Внутри промиса хранится результат вычисления, которое может быть уже выполнено или выполнится в будущем.

    Промис может находиться в одном из трёх состояний:

    • pending — стартовое состояние, операция стартовала;
    • fulfilled — получен результат;
    • rejected — ошибка.

    Поменять состояние можно только один раз: перейти из pending либо в fulfilled , либо в rejected :

    Схема трёх состояний промиса и переход между ними

    У промиса есть методы then ( ) и catch ( ) , которые позволяют использовать результат работы промиса.

    Как пишется

    Скопировать ссылку "Как пишется" Скопировано

    Промис создаётся с помощью конструктора.

    В конструктор передаётся функция-исполнитель асинхронной операции (англ. executor). Она вызывается сразу после создания промиса. Задача этой функции — выполнить асинхронную операцию и перевести состояние промиса в fulfilled (успех) или rejected (ошибка).

    Изменить состояние промиса можно, вызвав колбэки, переданные аргументами в функцию:

    2 колбэка Promise функции

     const promise = new Promise(function (resolve, reject)  const data = getData() // делаем асинхронную операцию: запрос в БД, API, etc. resolve(data) // переводим промис в состояние fulfilled. Результатом выполнения будет объект data>) const errorPromise = new Promise(function (resolve, reject)  reject(new Error('ошибка')) // переводим промис в состояние rejected. Результатом выполнения будет объект Error>) const promise = new Promise(function (resolve, reject)  const data = getData() // делаем асинхронную операцию: запрос в БД, API, etc. resolve(data) // переводим промис в состояние fulfilled. Результатом выполнения будет объект data >) const errorPromise = new Promise(function (resolve, reject)  reject(new Error('ошибка')) // переводим промис в состояние rejected. Результатом выполнения будет объект Error >)      
    • первый параметр (в примере кода назван resolve ) — колбэк для перевода промиса в состояние fulfilled , при его вызове аргументом передаётся результат операции;
    • второй параметр (в примере кода назван reject ) — колбэк для перевода промиса в состояние rejected , при его вызове аргументом передаётся информация об ошибке.

    Как понять

    Скопировать ссылку "Как понять" Скопировано

    Промис решает задачу выполнения кода, который зависит от результата асинхронной операции.

    Промис устроен таким образом, что рычаги управления его состоянием остаются у асинхронной функции. После создания, промис находится в состоянии ожидания pending . Когда асинхронная операция завершается, функция переводит промис в состояние успеха fulfilled или ошибки rejected .

    С помощью методов then ( ) , catch ( ) и finally ( ) мы можем реагировать на изменение состояния промиса и использовать результат его выполнения.

    Пример ниже показывает состояние промиса, который создаётся при нажатии на кнопку купить. Промис случайным образом завершается успехом или ошибкой:

    Методы

    Скопировать ссылку "Методы" Скопировано

    В работе мы чаще используем промисы, чем создаём. Использовать промис — значит выполнять код при изменении состояния промиса.

    Существует три метода, которые позволяют работать с результатом выполнения вычисления внутри промиса:

    • then ( )
    • catch ( )
    • finally ( )

    then

    Подробнее о работе then ( ) читайте в статье Promise. Метод then ( ) .

    Метод then ( ) используют, чтобы выполнить код после успешного выполнения асинхронной операции.

    Например, мы запросили у сервера список фильмов и хотим отобразить их на экране, когда сервер получит результат. В этом случае:

    • асинхронная операция — запрос данных у сервера;
    • код, который мы хотим выполнить после её завершения, — отрисовка списка.

    Метод then ( ) принимает в качестве аргумента две функции-колбэка. Если промис в состоянии fulfilled то выполнится первая функция. Если в состоянии rejected — вторая. Хорошей практикой считается не использовать второй аргумент метода then ( ) и обрабатывать ошибки при помощи метода catch ( ) .

     fetch(`https://swapi.dev/api/films/$/`).then(function (movies)  renderList(movies)>) fetch(`https://swapi.dev/api/films/$id>/`).then(function (movies)  renderList(movies) >)      

    В коде выше, асинхронная функция fetch ( ) возвращает промис, к которому применяется метод then . При его выполнении в переменной movies будет ответ сервера.

    catch

    Подробнее о работе catch ( ) читайте в статье Promise. Метод catch ( ) .

    Метод catch ( ) используют, чтобы выполнить код в случае ошибки при выполнении асинхронной операции.

    Например, мы запросили у сервера список фильмов и хотим показать экран обрыва соединения, если произошла ошибка. В этом случае:

    • асинхронная операция — запрос данных у сервера;
    • код, который мы хотим выполнить при ошибке — экран обрыва соединения.

    Метод catch ( ) принимает в качестве аргумента функцию-колбэк, которая выполняется сразу после того, как промис поменял состояние на rejected . Параметр колбэка содержит экземпляр ошибки:

     fetch(`https://swapi.dev/api/films/$/`).catch(function (error)  renderErrorMessage(error)>) fetch(`https://swapi.dev/api/films/$id>/`).catch(function (error)  renderErrorMessage(error) >)      

    В коде выше, асинхронная функция fetch ( ) возвращает промис, к которому применяется метод catch ( ) . При его выполнении в переменной error будет экземпляр ошибки.

    finally

    Подробнее о работе finally ( ) читайте в статье Promise. Метод finally ( ) .

    Метод finally ( ) используют, чтобы выполнить код при завершении асинхронной операции. Он будет выполнен вне зависимости от того, была ли операция успешной или завершилась ошибкой.

    Самый частый сценарий использования finally ( ) — работа с индикаторами загрузки. Перед началом асинхронной операции разработчик включает индикатор загрузки. Индикатор нужно убрать вне зависимости от того, как завершилась операция. Если этого не сделать, то пользователь не сможет взаимодействовать с интерфейсом.

    Метод finally ( ) принимает в качестве аргумента функцию-колбэк, которая выполняется сразу после того, как промис поменял состояние на rejected или fulfilled :

     let isLoading = truefetch(`https://swapi.dev/api/films/$/`).finally(function ()  isLoading = false>) let isLoading = true fetch(`https://swapi.dev/api/films/$id>/`).finally(function ()  isLoading = false >)      

    Цепочки методов

    Скопировать ссылку "Цепочки методов" Скопировано

    Методы then ( ) , catch ( ) и finally ( ) часто объединяют в цепочки вызовов, чтобы обработать и успешный, и ошибочный сценарии:

     let isLoading = true fetch(`https://swapi.dev/api/films/$/`) .then(function (movies)  renderList(movies) >) .catch(function (err)  renderErrorMessage(err) >) .finally(function ()  isLoading = false >) let isLoading = true fetch(`https://swapi.dev/api/films/$id>/`) .then(function (movies)  renderList(movies) >) .catch(function (err)  renderErrorMessage(err) >) .finally(function ()  isLoading = false >)      

    В этом случае при успешном завершении операции мы выполним код из then ( ) , при ошибке — код из catch ( ) . Затем выполнится код из finally ( ) .

    Цепочки методов — очень гибкий подход. Он позволяет создавать зависимые асинхронные операции.

    Например, нужно отобразить информацию о фильме и главном герое. Мы не знаем, кто главный герой, не получив эту информацию из данных о фильме. Таким образом, запрос данных о герое зависит от результата запроса данных о фильме.

    Промисы делают решение простым и читаемым. Мы можем начинать следующее асинхронное действие внутри колбэка метода then ( ) . Все, что возвращается из колбэка, оборачивается в промис, поэтому в цепочку можно добавить новый then ( ) :

     fetch(`https://swapi.dev/api/films/$/`) .then(function (response)  // этот then сработает, когда разрешится промис с запросом данных о фильме return response.json() // нужно распарсить ответ сервера, это асинхронная операция >) .then(function (movie)  // этот then сработает, когда данные о фильме распарсятся const characterUrl = movie.characters[0] return fetch(characterUrl) // вызов fetch вернет промис, возвращаем его из колбэка, чтобы продолжить цепочку >) .then(function (response)  // этот then сработает, когда разрешится промис с результатами запроса персонажа return response.json() >) .then(function (character)  renderCharacterProfile(character) >) .catch(function (err)  // catch сработает, когда любая из операций выше завершится ошибкой renderErrorMessage(err) >) fetch(`https://swapi.dev/api/films/$id>/`) .then(function (response)  // этот then сработает, когда разрешится промис с запросом данных о фильме return response.json() // нужно распарсить ответ сервера, это асинхронная операция >) .then(function (movie)  // этот then сработает, когда данные о фильме распарсятся const characterUrl = movie.characters[0] return fetch(characterUrl) // вызов fetch вернет промис, возвращаем его из колбэка, чтобы продолжить цепочку >) .then(function (response)  // этот then сработает, когда разрешится промис с результатами запроса персонажа return response.json() >) .then(function (character)  renderCharacterProfile(character) >) .catch(function (err)  // catch сработает, когда любая из операций выше завершится ошибкой renderErrorMessage(err) >)      

    Обработка ошибок в цепочках методов

    Скопировать ссылку "Обработка ошибок в цепочках методов" Скопировано

    Цепочки then ( ) при обработке промисов могут быть очень большими. В примере выше цепочка состоит из четырёх then ( ) и одного catch ( ) . Как в этом случае отработает catch ( ) ?

    ☝️ catch ( ) обрабатывает ошибки от всех then ( ) между ним и предыдущим catch ( ) .

    Один метод catch, поставленный в конце цепочки

    В примере выше наш catch ( ) — последний, а предыдущего нет, поэтому он будет обрабатывать все ошибки.

    Несколько методов catch, поставленных в середине и конце цепочки

    Если в цепочке несколько catch ( ) , то каждый ловит ошибки от then ( ) , находящихся выше.

    Один метод catch, поставленный в середине цепочки

    Возможен вариант, когда финального catch ( ) нет. Тогда ошибки от последних then ( ) не будут обрабатываться.

    ⚠️ Такой код — плохой. Если в одном из последних then ( ) произойдёт ошибка, то вся дальнейшая цепочка не отработает, при этом, из-за асинхронной природы промиса, прочий код вне промиса продолжит работать и приложение не упадёт.

    Другие полезные методы промисов

    Скопировать ссылку "Другие полезные методы промисов" Скопировано

    Иногда вам нужно обернуть уже известный результат вычисления в промис. Для этого вы можете использовать метод Promise . resolve ( ) :

     const happyDog = Promise.resolve('��')happyDog.then(function (dog)  console.log(dog) // ��>) const happyDog = Promise.resolve('��') happyDog.then(function (dog)  console.log(dog) // �� >)      

    Кроме этого есть метод Promise . reject ( ) , он используется реже. Обратите внимание, что результатом выполнения sad Dog . catch ( ) будет промис в статусе fulfilled

     const sadDog = Promise.reject('��')sadDog.catch(function (dog)  console.log(dog) // ��>) const sadDog = Promise.reject('��') sadDog.catch(function (dog)  console.log(dog) // �� >)      

    Как создать асинхронную функцию с промисом

    Скопировать ссылку "Как создать асинхронную функцию с промисом" Скопировано

    1. Создать функцию, которая будет выполнять асинхронную операцию:
     function earnAllMoney() <> function earnAllMoney() >      
    1. Вернуть из функции свежесозданный промис:
     function earnAllMoney()  return new Promise(function (resolve, reject) < /* . */ >)> function earnAllMoney()  return new Promise(function (resolve, reject)  /* . */ >) >      
    1. Аргументом в конструктор передать функцию, которая выполняет асинхронную операцию и переводит промис в состояние «успех» или «ошибка» в зависимости от результата:
     function earnAllMoney()  return new Promise(function (resolve, reject)  const result = tryEarnAllMoney() // асинхронная операция if (result.ok)  resolve(result) // успех → переводим промис в fulfilled и передаём результат > else  reject(new Error(result)) // ошибка → переводим промис в rejected > >)> function earnAllMoney()  return new Promise(function (resolve, reject)  const result = tryEarnAllMoney() // асинхронная операция if (result.ok)  resolve(result) // успех → переводим промис в fulfilled и передаём результат > else  reject(new Error(result)) // ошибка → переводим промис в rejected > >) >      

    �� Если асинхронная операция работает через колбэки, то её стоит обернуть в промис, чтобы писать более читаемый код.

    Рассмотрим пример с функцией get Data , которая принимает два колбэка: первый вызывается при успехе, второй — при ошибке. Код этой функции может выглядеть так:

     function getData(onSuccess, onError)  setTimeout(function ()  const result = Math.random() if (result > 0.5)  onSuccess(result) > else  onError(new Error('Что-то пошло не так')) > >, 1000)> function getData(onSuccess, onError)  setTimeout(function ()  const result = Math.random() if (result > 0.5)  onSuccess(result) > else  onError(new Error('Что-то пошло не так')) > >, 1000) >      

    Завернём её в промис:

     function promisifiedGetData()  return new Promise(function (resolve, reject)  const result = getData( function (result)  resolve(result) >, function (error)  reject(error) > ) >)> function promisifiedGetData()  return new Promise(function (resolve, reject)  const result = getData( function (result)  resolve(result) >, function (error)  reject(error) > ) >) >      

    Теперь можно использовать методы then ( ) и catch ( ) :

     promisifiedGetData() .then(function ()  console.log('success') >) .catch(function (err)  console.error(err.message) >) promisifiedGetData() .then(function ()  console.log('success') >) .catch(function (err)  console.error(err.message) >)      

    Промисы схлопываются

    Скопировать ссылку "Промисы схлопываются" Скопировано

    Интересная и удобная особенность промисов – если вложить один промис в другой они схлопнутся в один. Например:

     const promise = Promise.resolve(Promise.resolve(Promise.resolve('��')))// Promise : '��'>promise.then(console.log)// �� const promise = Promise.resolve(Promise.resolve(Promise.resolve('��'))) // Promise : '��'> promise.then(console.log) // ��      

    На практике

    Скопировать ссылку "На практике" Скопировано

    Николай Лопин советует

    Скопировать ссылку "Николай Лопин советует" Скопировано

    �� Промис становится «разрешённым» или «завершённым», когда он переходит из состояние pending в fulfilled или rejected . Состояние завершённого промиса нельзя поменять.

     const promise = new Promise(function (resolve, reject)  resolve() // в этот момент промис переходит в состояние fulfilled reject() // этот вызов игнорируется, потому что промис разрешился>) const promise = new Promise(function (resolve, reject)  resolve() // в этот момент промис переходит в состояние fulfilled reject() // этот вызов игнорируется, потому что промис разрешился >)      

    �� Всегда завершайте использование промиса методом catch ( ) . Если этого не сделать, то следующие промисы в цепочке перестанут работать, и такую ошибку получится поймать только через специальный обработчик – unhandledrejection .

    �� Время от времени нужно выполнить несколько асинхронных функций и дождаться, пока все выполнятся или одна из них завершится ошибкой. Для этого существует статический метод Promise . all ( ) (он возвращает промис).

    �� Если нужно дождаться пока несколько асинхронных функций завершатся (без разницы, успешно или ошибкой), используйте метод Promise . all Settled ( ) (вернёт промис).

    На собеседовании

    Скопировать ссылку "На собеседовании" Скопировано

    Использование промисов

    Promise (промис) - это объект, представляющий результат успешного или неудачного завершения асинхронной операции. Так как большинство людей пользуются уже созданными промисами, это руководство начнём с объяснения использования вернувшихся промисов до объяснения принципов создания.

    В сущности, промис - это возвращаемый объект, в который вы записываете два колбэка вместо того, чтобы передать их функции.

    Например, вместо старомодной функции, которая принимает два колбэка и вызывает один из них в зависимости от успешного или неудачного завершения операции:

    function doSomethingOldStyle(successCallback, failureCallback)  console.log("Готово."); // Успех в половине случаев. if (Math.random() > 0.5)  successCallback("Успех"); > else  failureCallback("Ошибка"); > > function successCallback(result)  console.log("Успешно завершено с результатом " + result); > function failureCallback(error)  console.log("Завершено с ошибкой " + error); > doSomethingOldStyle(successCallback, failureCallback); 

    …современные функции возвращают промис, в который вы записываете ваши колбэки:

    function doSomething()  return new Promise((resolve, reject) =>  console.log("Готово."); // Успех в половине случаев. if (Math.random() > 0.5)  resolve("Успех"); > else  reject("Ошибка"); > >); > const promise = doSomething(); promise.then(successCallback, failureCallback); 
    doSomething().then(successCallback, failureCallback); 

    Мы называем это асинхронным вызовом функции. У этого соглашения есть несколько преимуществ. Давайте рассмотрим их.

    Гарантии

    В отличие от старомодных переданных колбэков промис даёт некоторые гарантии:

    • Колбэки никогда не будут вызваны до завершения обработки текущего события в событийном цикле JavaScript.
    • Колбэки, добавленные через .then даже после успешного или неудачного завершения асинхронной операции, будут также вызваны.
    • Несколько колбэков может быть добавлено вызовом .then нужное количество раз, и они будут выполняться независимо в порядке добавления.

    Но наиболее непосредственная польза от промисов - цепочка вызовов (chaining).

    Цепочка вызовов

    Общая нужда - выполнять две или более асинхронных операции одна за другой, причём каждая следующая начинается при успешном завершении предыдущей и использует результат её выполнения. Мы реализуем это, создавая цепочку вызовов промисов (promise chain).

    Вот в чём магия: функция then возвращает новый промис, отличающийся от первоначального:

    let promise = doSomething(); let promise2 = promise.then(successCallback, failureCallback); 
    let promise2 = doSomething().then(successCallback, failureCallback); 

    Второй промис представляет завершение не только doSomething() , но и функций successCallback или failureCallback , переданных вами, а они тоже могут быть асинхронными функциями, возвращающими промис. В этом случае все колбэки, добавленные к promise2 будут поставлены в очередь за промисом, возвращаемым successCallback или failureCallback .

    По сути, каждый вызванный промис означает успешное завершение предыдущих шагов в цепочке.

    Раньше выполнение нескольких асинхронных операций друг за другом приводило к классической "Вавилонской башне" колбэков:

    doSomething(function (result)  doSomethingElse( result, function (newResult)  doThirdThing( newResult, function (finalResult)  console.log("Итоговый результат: " + finalResult); >, failureCallback, ); >, failureCallback, ); >, failureCallback); 

    В современных функциях мы записываем колбэки в возвращаемые промисы - формируем цепочку промисов:

    doSomething() .then(function (result)  return doSomethingElse(result); >) .then(function (newResult)  return doThirdThing(newResult); >) .then(function (finalResult)  console.log("Итоговый результат: " + finalResult); >) .catch(failureCallback); 

    Аргументы then необязательны, а catch(failureCallback) - это сокращение для then(null, failureCallback) . Вот как это выражено с помощью стрелочных функций:

    doSomething() .then((result) => doSomethingElse(result)) .then((newResult) => doThirdThing(newResult)) .then((finalResult) =>  console.log(`Итоговый результат: $finalResult>`); >) .catch(failureCallback); 

    Важно: Всегда возвращайте промисы в return, иначе колбэки не будут сцеплены и ошибки могут быть не пойманы (стрелочные функции неявно возвращают результат, если скобки <> вокруг тела функции опущены).

    Цепочка вызовов после catch

    Можно продолжить цепочку вызовов после ошибки, т. е. после catch , что полезно для выполнения новых действий даже после того, как действие вернёт ошибку в цепочке вызовов. Ниже приведён пример:

    new Promise((resolve, reject) => < console.log('Начало'); resolve(); >) .then(() => < throw new Error('Где-то произошла ошибка'); console.log('Выведи это'); >) .catch(() => < console.log('Выведи то'); >) .then(() => < console.log('Выведи это, несмотря ни на что'); >);

    В результате выведется данный текст:

    Начало Выведи то Выведи это, несмотря ни на что

    Заметьте, что текст "Выведи это" не вывелся, потому что "Где-то произошла ошибка" привела к отказу

    Распространение ошибки

    Вы могли ранее заметить, что failureCallback повторяется три раза в "pyramid of doom", а в цепочке промисов всего лишь один раз:

    doSomething() .then(result => doSomethingElse(result)) .then(newResult => doThirdThing(newResult)) .then(finalResult => console.log(`Итоговый результат: $`)) .catch(failureCallback);

    В основном, цепочка промисов останавливает выполнение кода, если где-либо произошла ошибка, и вместо этого ищет далее по цепочке обработчики ошибок. Это очень похоже на то, как работает синхронный код:

    try < let result = syncDoSomething(); let newResult = syncDoSomethingElse(result); let finalResult = syncDoThirdThing(newResult); console.log(`Итоговый результат: $`); > catch(error)

    Эта симметрия с синхронным кодом лучше всего показывает себя в синтаксическом сахаре async / await в ECMAScript 2017:

    async function foo() < try < let result = await doSomething(); let newResult = await doSomethingElse(result); let finalResult = await doThirdThing(newResult); console.log(`Итоговый результат: $`); > catch(error) < failureCallback(error); >>

    Работа данного кода основана на промисах. Для примера здесь используется функция doSomething() , которая встречалась ранее. Вы можете прочитать больше о синтаксисе здесь

    Промисы решают основную проблему пирамид, обработку всех ошибок, даже вызовов исключений и программных ошибок. Это основа для функционального построения асинхронных операций.

    Создание промиса вокруг старого колбэка

    Promise может быть создан с помощью конструктора. Это может понадобится только для старых API.

    В идеале, все асинхронные функции уже должны возвращать промис. Но увы, некоторые APIs до сих пор ожидают успешного или неудачного колбэка переданных по старинке. Типичный пример: setTimeout() функция:

    setTimeout(() => saySomething("10 seconds passed"), 10000);

    Смешивание старого колбэк-стиля и промисов проблематично. В случае неудачного завершения saySomething или программной ошибки, нельзя обработать ошибку.

    К счастью мы можем обернуть функцию в промис. Хороший тон оборачивать проблематичные функции на самом низком возможном уровне, и больше никогда их не вызывать напрямую:

    const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

    В сущности, конструктор промиса становится исполнителем функции, который позволяет нам резолвить или режектить промис вручную. Так как setTimeout всегда успешен, мы опустили reject в этом случае.

    Композиция

    Promise.resolve() и Promise.reject() короткий способ создать уже успешные или отклонённые промисы соответственно. Это иногда бывает полезно.

    Promise.all() и Promise.race() - два метода запустить асинхронные операции параллельно.

    Последовательное выполнение композиции возможно при помощи хитрости JavaScript:

    [func1, func2].reduce((p, f) => p.then(f), Promise.resolve());

    Фактически, мы превращаем массив асинхронных функций в цепочку промисов равносильно: Promise.resolve().then(func1).then(func2);

    Это также можно сделать, объединив композицию в функцию, в функциональном стиле программирования:

    const applyAsync = (acc,val) => acc.then(val); const composeAsync = (. funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

    composeAsync функция примет любое количество функций в качестве аргументов и вернёт новую функцию которая примет в параметрах начальное значение, переданное по цепочке. Это удобно, потому что некоторые или все функции могут быть либо асинхронными, либо синхронными, и они гарантированно выполнятся в правильной последовательности:

    const transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2); transformData(data);

    В ECMAScript 2017, последовательные композиции могут быть выполнены более простым способом с помощью async/await:

    for (const f of [func1, func2])

    Порядок выполнения

    Чтобы избежать сюрпризов, функции, переданные в then никогда не будут вызваны синхронно, даже с уже разрешённым промисом:

    Promise.resolve().then(() => console.log(2)); console.log(1); // 1, 2

    Вместо немедленного выполнения, переданная функция встанет в очередь микрозадач, а значит выполнится, когда очередь будет пустой в конце текущего вызова JavaScript цикла событий (event loop), т.е. очень скоро:

    const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); wait().then(() => console.log(4)); Promise.resolve().then(() => console.log(2)).then(() => console.log(3)); console.log(1); // 1, 2, 3, 4

    Вложенность

    Простые цепочки promise лучше оставлять без вложений, так как вложенность может быть результатом небрежной структуры. Смотрите распространённые ошибки.

    Вложенность - это управляющая структура, ограничивающая область действия операторов catch. В частности, вложенный catch только перехватывает сбои в своей области и ниже, а не ошибки выше в цепочке за пределами вложенной области. При правильном использовании это даёт большую точность в извлечение ошибок:

    doSomethingCritical() .then(result => doSomethingOptional() .then(optionalResult => doSomethingExtraNice(optionalResult)) .catch(e => <>)) // Игнорируется если необязательные параметр не выкинул исключение .then(() => moreCriticalStuff()) .catch(e => console.log("Критическая ошибка: " + e.message));

    Обратите внимание, что необязательный шаги здесь выделены отступом.

    Внутренний оператор catch нейтрализует и перехватывает ошибки только от doSomethingOptional() и doSomethingExtraNice(), после чего код возобновляется с помощью moreCriticalStuff(). Важно, что в случае сбоя doSomethingCritical() его ошибка перехватывается только последним (внешним) catch.

    Частые ошибки

    В этом разделе собраны частые ошибки, возникающие при создании цепочек промисов. Несколько таких ошибок можно увидеть в следующем примере:

    // Плохой пример! Три ошибки! doSomething().then(function(result) < doSomethingElse(result) // Забыл вернуть промис из внутренней цепочки + неуместное влаживание .then(newResult =>doThirdThing(newResult)); >).then(() => doFourthThing()); // Забыл закончить цепочку методом catch

    Первая ошибка это неправильно сцепить вещи между собой. Такое происходит когда мы создаём промис но забываем вернуть его. Как следствие, цепочка сломана, но правильнее было бы сказать что теперь у нас есть две независимые цепочки, соревнующиеся за право разрешится первой. Это означает, что doFourthThing() не будет ждать doSomethingElse() или doThirdThing() пока тот закончится, и будет исполнятся параллельно с ними, это, вероятно, не то что хотел разработчик. Отдельные цепочки также имеют отдельную обработку ошибок, что приводит к необработанным ошибкам.

    Вторая ошибка это излишняя вложенность, включая первую ошибку. Вложенность также ограничивает область видимости внутренних обработчиков ошибок, если это не то чего хотел разработчик, это может привести к необработанным ошибкам. Примером этого является пример как не нужно создавать промисы, который комбинирует вложенность с чрезмерным использованием конструктора промисов для оборачивания кода который уже использует промисы.

    Третья ошибка это забыть закончить цепочку ключевым словом catch . Незаконченные цепочки приводят к необработанным отторжениям промисов в большинстве браузеров.

    Хорошим примером является всегда либо возвращать либо заканчивать цепочки промисов, и как только вы получаете новый промис, возвращайте его сразу же, чтобы не усложнять код излишней вложенностью:

    doSomething() .then(function(result) < return doSomethingElse(result); >) .then(newResult => doThirdThing(newResult)) .then(() => doFourthThing()) .catch(error => console.log(error));

    Обратите внимание что () => x это сокращённая форма () => < return x; >.

    Теперь у нас имеется единственная определённая цепочка с правильной обработкой ошибок.

    Использование async / await предотвращает большинство, если не все вышеуказанные ошибки, но взамен появляется другая частая ошибка — забыть ключевое слово await .

    Смотрите также

    • Promise.then()
    • Спецификация Promises/A+ (EN)
    • Нолан Лоусон (Nolan Lawson): У нас проблемы с промисами - распространённые ошибки (EN)

    Found a content problem with this page?

    • Edit the page on GitHub.
    • Report the content issue.
    • View the source on GitHub.

    Promises — JS: Синхронная асинхронность

    Промисы стали настоящим спасением человечества и среди прогрессивных разработчиков являются основным способом управления асинхронным кодом.

    Полное описание всех возможностей и аспектов поведения промисов является объемной задачей, которая может запутать на первых порах, поэтому в этом уроке мы остановимся на ключевых особенностях поведения. Все остальное можно почерпнуть из стандарта и/или документации.

    Знакомству с промисами способствует понимание темы "конечные автоматы".

    Начнем по традиции с примера:

    const file = '/tmp/hello1.txt'; import  writeFile, readFile > from 'fs-promise'; writeFile(file, 'hello world') .then(() => readFile(file, 'utf8')) .then(contents => console.log(contents)) .catch(err => console.log(err)); // hello world 

    В этом примере происходит запись файла, затем чтение этого же файла, затем вывод содержимого этого файла в консоль, а в случае ошибки, возникшей на любом этапе, она была бы выведена на экран.

    Абзац выше – это пример того, как выглядит типичная программа, построенная на промисах. Так что такое промис?

    Объект, используемый для асинхронных операций. Промис содержит в себе результат выполнения и позволяет строить цепочки из вычислений, избегая проблемы callback hell

    • Promise.prototype.then(onFulfilled, onRejected)
    • Promise.prototype.catch(onRejected)

    Отсутствие callback hell происходит благодаря тому, что мы всегда работаем на уровне последовательных вызовов then , а не уходим в глубину.

    Разберем пример выше по косточкам. Первый вызов writeFile(file, 'hello world') возвращает тот самый промис, и пока не важно, как он строится внутри, сейчас мы пытаемся понять то, как с ним работать.

    // Вызов ничем не отличается кроме того, что мы не передаем колбек writeFile(file, 'hello world') 

    После этого у нас есть два варианта:

    • Мы вызываем then и передаем функцию onFulfilled , которая будет вызвана в случае успешного выполнения асинхронной операции
    • Мы вызываем catch и передаем функцию onRejected , которая будет вызвана, в случае ошибок в результате выполнения асинхронной операции.

    Функция onFulfilled принимает на вход данные, которые были получены в результате предыдущего выполнения. Таким образом идет передача данных по цепочке.

    .then(() => readFile(file, 'utf8')) .then(contents => console.log(contents)) 

    Данные, возвращаемые из функции onFulfilled , переходят по цепочке в функцию onFulfilled следующего then . Но если вернуть promise , то в следующем then окажутся данные, полученные в результате выполнения этого промиса, а не сам промис. Что и происходит в примере выше: мы возвращаем readFile() , а ниже получаем contents . То есть, промисы хорошо комбинируются друг с другом.

    Конечный автомат

    Теперь попробуем посмотреть внутрь промиса. С концептуальной точки зрения промис – это конечный автомат, у которого три состояния: pending , fulfilled , rejected .

    Изначально он находится в состоянии pending , а дальше может перейти в одно из двух: либо выполнен ( fulfilled ), либо отклонен ( rejected ). И все, больше никакие переходы невозможны. Придя один раз в одно из терминальных (конечных) состояний, промис больше не подвержен изменениям, как бы мы не старались снаружи заставить его перейти в другое состояние.

    Реализация

    const promiseReadFile = filename =>  return new Promise((resolve, reject) =>  fs.readFile(filename, (err, data) =>  err ? reject(err) : resolve(data); >); >); >; 

    Любая функция возвращающая промис, внутри себя создает объект промиса привычным способом. Конструктор Promise принимает на вход функцию, внутри которой запускается выполнение асинхронной операции. Делается это, кстати, сразу, промисы не являются примером отложенного (lazy) выполнения кода. Но это еще не все. Промис требует от нас некоторых действий для своей работы. Во входную функцию передаются две другие: reject и resolve . reject должна быть вызвана в случае ошибки с передачей внутрь объекта error , а resolve — в случае успешного завершения асинхронной операции с передачей внутрь данных, если они есть.

    Ошибки

    Ошибка обрабатывается ближайшим обработчиком onRejected в цепочке вызовов. При этом существует два варианта определения обработчика. Первый - через catch , второй - с помощью передачи в then второго параметра. Это продемонстрировано в примере ниже:

    promiseReadFile('file1') .then(data => promiseWriteFile('file2', data)) .then(() => promiseReadFile('file3')) .then(data => console.log(data)) .catch(err => console.log(err)); // .then(null, err => console.log(err)); 

    Promise.all

    Иногда возникает необходимость дождаться выполнения нескольких асинхронных операций. В этом случае можно воспользоваться идиомой Promise.all . Работает она очень просто: в эту функцию передается массив промисов, а дальше в then приходит массив с результатами выполнения.

    const readJsonFiles = filenames =>  // N.B. passing readJSON as a function, // not calling it with `()` return Promise.all(filenames.map(readJSON)); > readJsonFiles(['a.json', 'b.json']) .then(results =>  // results is an array of the values 

    Открыть доступ

    Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

    • 130 курсов, 2000+ часов теории
    • 1000 практических заданий в браузере
    • 360 000 студентов

    Наши выпускники работают в компаниях:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *