Если вы занимаетесь веб-разработкой на JavaScript, я думаю, вы уже знакомы с Promise и неоднократно сталкивались с адом обратных вызовов. Цепочка промисов в JavaScript — это один из способов решить проблему ада обратных вызовов, и мы обсудим его в этой статье. Тем не менее, давайте немного подытожим для тех, кто не знаком с понятиями.

Что такое JavaScript Promise?

В JavaScript (ES6 и выше) Promise — это объект, представляющий состояние и результат асинхронных операций, таких как вызов API или чтение/запись ввода-вывода. Состояния включают ожидание, выполнено и отклонено.

  • Ожидание: операция выполняется, и результат не возвращен.
  • Выполнено: операция прошла успешно, результат возвращен.
  • Отклонено: операция завершилась неудачно, и была возвращена ошибка.

Promise имеет два метода: then и catch. Метод then принимает обратный вызов действия, которое должно быть инициировано после выполнения обещания, в то время как catch запускается всякий раз, когда обещание отклоняется.

// Asynchronous API call operation
getWeatherTodayPromise
   .then((weatherForecast) => { // Fulfilled
      // Synchronous operation
      display(weatherForecast)
   })
   .catch((error) = > { // Rejected
      console.error(error)
   })

Что такое ад обратного вызова?

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

// A typical callback hell which involves multiple JavaScript promises
firstPromise
   .then(secondPromise
      .then(thirdPromise
         .then(...).catch(...)
      ).catch(...)
   ).catch(...)

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

// Asynchronous API call operation
getWeatherTodayPromise
   .then((weatherForecast) => {
      // Asynchronous IO operation
      writeWeatherForecastToLogFilePromise(weatherForecast) // FAILED
          ...
         // Unlike try-catch, there is no outer "catch-all" solution
         // You must have a "catch" at every nested promise
         // Or else, the promise is not terminated, and the error information is lost
         .catch((error) => {
            // IO error is caught here
            console.error("Inner promise", error)
         })
   })
   .catch((error) = > {
       // IO error is NOT caught here
       console.error("Outer promise", error)
   })

Что такое цепочка промисов в JavaScript?

Вы только что узнали, что ад обратных вызовов указывает на неуправляемые вложенные уровни обратных вызовов. С учетом сказанного, один из способов решить проблему - сделать обратные вызовы НЕ вложенными. Сделайте его мелким!

Цепочка обещаний в JavaScript — это когда несколько методов then и catch вызываются последовательно, чтобы полностью удалить вложенные уровни, сохраняя при этом предполагаемые результаты. Можно сказать, что это один из способов рефакторинга вашей кодовой базы.

// Promise chaining
firstPromise
   .then(() => secondPromise)
   .then(() => thirdPromise)
   .then(...)
   .catch(...)
   .catch(...)
   .catch(...)
   .then(...)
// A typical callback hell which involves multiple JavaScript promises
firstPromise
   .then(secondPromise
      .then(thirdPromise
         .then(...).catch(...)
      ).catch(...)
   ).catch(...)

Если вы имеете дело с обычными функциями на основе обратного вызова (НЕ промисами), вам нужно сначала промисифицировать функции, чтобы применить цепочку промисов. Есть способы сделать это, например, с помощью библиотеки es6-promisify.

Как работает цепочка промисов в JavaScript?

  • then и catch — это методы объекта Promise, поэтому для создания цепочки обратный вызов в методе then должен возвращать новый Promise.
// Correct implementation
// "() => something" is a shorthand for "() => {return something})
const secondPromise = firstPromise.then(() => newPromise)
secondPromise.then(() => anotherPromise).then(...)
// Wrong implementation and an exception is raised
firstPromise.then(() => null).then(...)
  • Результат обещания переносится на следующий then.
getWeatherTodayPromise
   .then(weatherForecastResult => writeWeatherForecastToLogFilePromise(weatherForecastResult))
   // writeWeatherForecastToLogFilePromise(weatherForecastResult) if fulfilled will provide "ioWriteResult"
   .then(ioWriteResult => Promise.all([
      anotherPromise(ioWriteResult),
      andSomethingElsePromise(),
    ]))
   .then(listOfResults => ...)
  • Мы можем вручную инициировать и вернуть новый объект Promise, чтобы сформировать цепочку промисов. Вместо того, чтобы выполнять задачи в одном обратном вызове, вы можете применить эту технику для сегментации кодовой базы на более мелкие фрагменты.
firstPromise
   .then(() => {
       const isSuccess = synchronousOperation() // boolean
       return isSuccess ? Promise.resolve("Success") : Promise.reject(new Error("404"))
    })
    .then((result) => console.log(result)) // Print "Success"
    .catch(error) => console.error(error)) // Print an error with "404" message
  • Метод then имеет второй и необязательный аргумент onRejectedCallback, но поскольку мы его не используем, всякий раз, когда возникает исключение, браузер просматривает всю цепочку промисов, чтобы найти первый приемлемый catch для данной ошибки.
  • Используя цепочку промисов, вы можете иметь одно внешнее «универсальное» решение, такое как try-catch, поэтому больше не будет возможности проглатывания ошибок. У вас может быть условный оператор в одном catch для нескольких ошибок, или вы можете разделить их на несколько разделенных catch, как показано ниже.
rejected5xxPromise
   .catch(HTTP 4xx) // Browser: "Not here"
   .catch(HTTP 5xx) // Browser: "Okay, this catches the 5xx error"
   .catch(Other unexpected errors) // Skip
  • Вы можете связать then после catch. Это означает «всегда действовать, несмотря ни на что».
rejected5xxPromise
   .catch(HTTP 5xx)
   .then(console.log("This line is always printed out"))

Заворачивать

Цепочка обещаний JavaScript — это простая, но мощная функция для решения распространенной проблемы с вложенными обратными вызовами (ад обратных вызовов). Чтобы связать обещания, нужно помнить два основных момента.

  1. Можно последовательно вызвать несколько then и catch, например promise.then(...).then(...).catch(...).catch(...).
  2. Обратный вызов в методе then должен возвращать новый объект Promise, чтобы цепочка могла быть продолжена.

Это правило также относится к TypeScript. В ES2016 (он же ES7) была введена функция async/await, которая делает нашу жизнь еще проще. Тем не менее, если вы по какой-то причине не можете использовать функцию ES7, то цепочка промисов — отличный выбор для рефакторинга вашей кодовой базы.

Заинтересованы в веб-разработке? Другие мои статьи могут быть вам полезны!