Event Loop, Callbacks, Promises, и Async/Await, часть 3 из 4

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

Обещания (Promises)

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

Создание обещания (Promise)

Promise инициализируется с помощью new Promise(fn), и ему следует передавать функцию. Функция, которая передается в обещание, имеет параметры для разрешения (resolve) и отклонения (reject). Функции resolve и reject обрабатывают, соответственно, успех и неудачу операции. Пример:

// Инициализация Promise
const promise = new Promise((resolve, reject) => {})

Если проверить инициализированный promise в этом состоянии с помощью консоли веб-браузера, можно обнаружить, что оно имеет статус pending и значение undefined:

__proto__: Promise
[[PromiseStatus]]: "pending"
[[PromiseValue]]: undefined

Пока для promise ничего не настроено, оно будет оставаться в состоянии pending. Первое, что уже можно сделать, чтобы проверить promise — это выполнить promise, разрешив его с каким-нибудь значением:

const promise = new Promise((resolve, reject) => {
  resolve('Мы сделали это!')
})

Теперь, после проверки, можно обнаружить, что статус promise изменился на fulfilled, а у value значению, которое было передано в функцию resolve:

__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: "Мы сделали это!"

Как указано в начале этого раздела, promise — это объект, который может возвращать значение. После успешного выполнения содержимое value меняется с undefined на заполненное данными. У обещания бывает три возможных состояния: отложено (pending), выполнено (fulfilled) и отклонено (rejected).

  • Pending — исходное состояние до разрешения или отклонения
  • Fulfilled — успешная операция, обещание разрешено
  • Rejected — неудачная операция, обещание отклонено

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

Использование обещания (Promise)

Promise в последнем примере выполнено со значением, поэтому можно получить доступ к этому значению. У обещаний есть метод then, который выполнится после того, как в коде оно доберётся до resolve.then вернёт значение promise в качестве параметра. Вот пример, как это можно сделать:

promise.then(response => {
  console.log(response)
})

Значением [[PromiseValue]] из примера выше было «Мы сделали это!». Это значение будет передано анонимной функции в качестве response:

Мы сделали это!

До сих пор этот пример не включал асинхронный веб-API и только объяснял, как создавать, обрабатывать и использовать встроенное обещание (promise). Используя setTimeout, можно имитировать асинхронный запрос и посмотреть, как это работает:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Разрешение асинхронного запроса!'), 2000)
})

// Результат
promise.then(response => {
  console.log(response)
})

Использование синтаксиса then гарантирует, что response попадёт в консоль только через 2000 миллисекунд, когда операция setTimeout будет завершена. Все это делается без вложенных обратных вызовов (callback).

Теперь, через две секунды, разрешится значение promise и then напишет в консоль

Разрешение асинхронного запроса!

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

// promise-цепочка
promise
  .then(firstResponse => {
    // возвращает новое значение для следующего then
    return firstResponse + ' И цепочка!'
  })
  .then(secondResponse => {
    console.log(secondResponse)
  })

Выполненное во втором then обещание напишет в консоль:/p>

Разрешение асинхронного запроса! И цепочка!

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

Обработка ошибок

До сего времени рассматривались примеры promise только с успешным разрешением resolve, которое переводит обещание в состояние fullfilled (выполнено). Но часто с асинхронным запросом необходимо обрабатывать ошибки — если API не работает, отправляется неверный или неавторизованный запрос. Promise должен обрабатывать оба случая.

В этом примере в функцию getUsers передается флаг, а она возвращает promise.

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // имитация разрешения и отклонения в асинхронном API
    }, 1000)
  })
}

Теперь дополняем код так, чтобы, когда в onSuccess передано true, таймаут выполнялся с некоторыми данными. Если false, promise будет отклонен с ошибкой.

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // имитация разрешения и отклонения в асинхронном API
      if (onSuccess) {
        resolve([
          { id: 1, name: 'Пётр' },
          { id: 2, name: 'Фёдор' },
          { id: 3, name: 'Егор' },
        ])
      } else {
        reject('Ошибка получения данных!')
      }
    }, 1000)
  })
}

Для успешного результата возвращается JavaScript-объект, который имитирует список пользователей.

Для того, чтобы обработать ошибку, следует использовать метод catch. Он возвращает ошибку, которую можно обработать в передаваемом ему в ​​качестве параметра обратном вызове.

Для примера, вызываем функцию getUser с параметром onSuccess, установленным в false, используем метод then для обработки успеха и метод catch для ошибки:

// чтобы имитировать ошибку,
// надо запустить функции getUsers с флагом false
getUsers(false)
  .then(response => {
    console.log(response)
  })
  .catch(error => {
    console.error(error)
  })

Поскольку promise был отклонен и имитирована ошибка, then будет пропущен, а ошибка будет обработана в catch:

Ошибка получения данных!

Если флаг, передаваемый в getUser переключить в true, promise будет разрешено успешно, выполнится код внутри then, а catch игнорироваться.

// чтобы имитировать успех,
// надо запустить функции getUsers с флагом true
getUsers(true)
  .then(response => {
    console.log(response)
  })
  .catch(error => {
    console.error(error)
  })

B будет имитироваться асинхронное получение списка пользователей:

(3) [{…}, {…}, {…}]
0: {id: 1, name: "Пётр"}
1: {id: 2, name: "Фёдор"}
3: {id: 3, name: "Егор"}

Подведём итог:

then()
Обрабатывает resolve. Возвращает promise и асинхронно вызывает функцию onFulfilled.
catch()
Обрабатывает reject. Возвращает promise и асинхронно вызывает функцию onRejected.
finally()
Вызывается, когда обещание выполнено. Возвращает promise и вызывает асинхронную функцию onFinally.

Promise могут сбивать с толку как новичков, так и опытных программистов, которые никогда раньше не работали в асинхронной среде. Однако, как уже упоминалось, гораздо чаще приходится использовать promise, чем создавать их. Обычно веб-API браузера или сторонняя библиотека предоставляют promise, и остается только использовать его.

В последнем разделе о promise в этом руководстве будет приведен типичный пример использования веб-API, который возвращает обещания: Fetch API.

Использование Fetch API с promise

Одним из наиболее полезных и часто используемых веб-API, возвращающих promise, является Fetch API, который позволяет выполнять асинхронные сетевые запросы. fetch — это процесс, состоящий из двух частей, поэтому он требует цепочки then. В этом примере демонстрируется использование API GitHub для получения данных пользователя, а также обработка любой потенциальной ошибки:

// Получить пользователя из API GitHub
fetch('https://api.github.com/users/octocat')
  .then(response => {
    return response.json()
  })
  .then(data => {
    console.log(data)
  })
  .catch(error => {
    console.error(error)
  })

Асинхронный запрос методом fetch отправляется на URL https://api.github.com/users/octocat и ожидает получения ответа. Первый then передает ответ анонимной функции, которая отформатирует ответ в виде JSON, затем передает этот JSON второму then, который отправляет данные в консоль. Оператор catch записывает любую ошибку в консоль. Выполнение этого кода возвращает следующее:

login: "octocat",
id: 583231,
avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4"
blog: "https://github.blog"
company: "@github"
followers: 3203
...

Это данные, запрошенные с https://api.github.com/users/octocat, представленные в формате JSON.

В этом разделе руководства показано, что promise включают в себя множество улучшений для работы с асинхронным кодом. Хотя использование then для обработки асинхронных действий легче, чем пирамида обратных вызовов (callback), некоторые разработчики по-прежнему предпочитают синхронный формат написания асинхронного кода. Чтобы удовлетворить эту потребность и упростить работу с обещаниями, ECMAScript 2016 (ES7) предлагает функцию async и ключевое слово await. В следующей части рассматривается использование async/await.