[Перевод] Async/Await делает ваш код более лаконичным

Node.js JavaScript Promise Ajax

перед статьей

Эта статья переведена сASYNC/AWAIT WILL MAKE YOUR CODE SIMPLER, Это статья, написанная в августе 2017 года и названная колонкой одной из десяти статей, которые необходимо прочитать в 2017 году. Я не смог найти перевод этой статьи на Наггетсах (на самом деле, я не искал его тщательно), поэтому я хотел попробовать перевести его самостоятельно. Если перевод не очень хороший, я также надеюсь, что все могут указать, что для меня хорошо, что я не ставлю под сомнение уровень Наггетс (последняя статья прокомментировала мое сердце  ̄▽ ̄), спасибо.

[Перевод] Async/Await делает ваш код более лаконичным

Или как научиться не использовать функцию обратного вызова и влюбиться в ES8

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

задний план

AJAX (Asynchronous JavaScript And XML)

Во-первых, давайте проведем небольшое исследование. В конце 90-х Ajax стал первым крупным прорывом в асинхронном JavaScript. Этот метод позволяет веб-сайтам извлекать и отображать новые данные после загрузки html. В то время это было революционным нововведением для большинства веб-сайтов, которое требовало повторной загрузки всей страницы для отображения частичного содержимого. Эта технология (известная как связывание в качестве вспомогательной функции в jQuery) доминировала в веб-разработке на протяжении 21 века, в то время как ajax сегодня является основной технологией, используемой веб-сайтами для извлечения данных, но xml был массово заменен json.

NodeJS

Когда NodeJS был впервые выпущен в 2009 году, основной проблемой на стороне сервера было то, чтобы программы могли изящно обрабатывать параллелизм. Большинство серверных языков в то время обрабатывали операции ввода-вывода таким образом, что завершение кода блокировалось до тех пор, пока не завершится обработка операции ввода-вывода, прежде чем продолжить выполнение предыдущего кода. Вместо этого NodeJS использует систему циклов событий и работает по принципу ajax: после завершения неблокирующей асинхронной операции может быть запущена функция обратного вызова, назначенная разработчиком.

Promises

Несколько лет спустя в средах nodejs и браузеров появился новый стандарт под названием «promises», который предоставил более мощный и стандартизированный способ построения асинхронных операций. Обещания по-прежнему используют формат на основе обратного вызова, но предоставляют унифицированный синтаксис для создания цепочек и создания асинхронных операций. Promises, стандарт, созданный популярной библиотекой с открытым исходным кодом, был наконец добавлен в нативный JavaScript в 2015 году.

Хотя это обещает значительное улучшение, но все же генерирует код трудно читать в некоторых случаях.

Теперь у нас есть новое решение.

async/await — это новый синтаксис (заимствованный из .net и c#), который позволяет нам создавать промисы как обычные функции без обратных вызовов. Это отличное дополнение к JavaScript, которое было добавлено в JavaScript ES7 в прошлом году, и его даже можно использовать для упрощения практически любого существующего js-приложения.

Examples

Мы приведем несколько примеров.

Эти примеры кода не требуют загрузки сторонних библиотек. **Async/await полностью поддерживается в последних версиях Chrome, Firefox, Safari и Edge, поэтому вы можете попробовать запустить эти примеры в консоли браузера. **Кроме того, синтаксис async/await работает на Node 7.6 и выше, а Babel и TypeScript также поддерживают синтаксис async/await. Async и await теперь можно использовать в любом проекте JavaScript.

Setup

Если вы хотите пойти по нашим стопам и изучить асинхронность на своем компьютере, мы будем использовать этот виртуальный класс API. Этот класс имитирует процесс вызова сети, возвращая объекты обещаний, и эти объекты обещаний будут использовать функцию разрешения для передачи простых данных в качестве параметров через 200 мс после вызова.

class Api {
  constructor () {
    this.user = { id: 1, name: 'test' }
    this.friends = [ this.user, this.user, this.user ]
    this.photo = 'not a real photo'
  }

  getUser () {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(this.user), 200)
    })
  }

  getFriends (userId) {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(this.friends.slice()), 200)
    })
  }
  
  getPhoto (userId) {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(this.photo), 200)
    })
  }

  throwError () {
    return new Promise((resolve, reject) => {
      setTimeout(() => reject(new Error('Intentional Error')), 200)
    })
  }
}

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

Первая попытка - вложенная функция обратного вызова обещания

Вот как это реализовать с помощью вложенных обратных вызовов обещаний.

function callbackHell () {
  const api = new Api()
  let user, friends
  api.getUser().then(function (returnedUser) {
    user = returnedUser
    api.getFriends(user.id).then(function (returnedFriends) {
      friends = returnedFriends
      api.getPhoto(user.id).then(function (photo) {
        console.log('callbackHell', { user, friends, photo })
      })
    })
  })
}

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

 		})
    })
  })
}

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

Что еще хуже, нет способа выполнить проверку ошибок, поэтому любой обратный вызов может незаметно завершиться ошибкой как необработанный отказ от промиса.

Вторая попытка - Связанные обещания

Посмотрим, сможем ли мы улучшить

function promiseChain () {
  const api = new Api()
  let user, friends
  api.getUser()
    .then((returnedUser) => {
      user = returnedUser
      return api.getFriends(user.id)
    })
    .then((returnedFriends) => {
      friends = returnedFriends
      return api.getPhoto(user.id)
    })
    .then((photo) => {
      console.log('promiseChain', { user, friends, photo })
    })
}

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

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

Третья попытка Async/Await

Возможно ли, что мы не используем никаких функций обратного вызова? Это невозможно?Любые мысли о возможности реализации всего в 7 строк?

async function asyncAwaitIsYourNewBestFriend () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)
  const photo = await api.getPhoto(user.id)
  console.log('asyncAwaitIsYourNewBestFriend', { user, friends, photo })
}

Стало лучше? Вызов await перед обещанием приостанавливает выполнение функции до тех пор, пока обещание не будет разрешено, а затем присваивает результат переменной слева от знака равенства. Этот метод позволяет нам написать поток асинхронной операции, как обычную синхронную команду.

Я думаю, вы так же взволнованы этой функцией, как и я прямо сейчас? !

Обратите внимание, что ключевое слово "async" объявлено в начале всего объявления функции. Мы должны сделать это, потому что это фактически превращает всю функцию в обещание. Мы изучим его позже.

ПЕТЛИ (петля)

Async/await упрощает очень сложные операции, например, как насчет добавления списка друзей, которых мы хотим получить по порядку для каждого пользователя?

Первая попытка - рекурсивный цикл промисов

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

function promiseLoops () {  
  const api = new Api()
  api.getUser()
    .then((user) => {
      return api.getFriends(user.id)
    })
    .then((returnedFriends) => {
      const getFriendsOfFriends = (friends) => {
        if (friends.length > 0) {
          let friend = friends.pop()
          return api.getFriends(friend.id)
            .then((moreFriends) => {
              console.log('promiseLoops', moreFriends)
              return getFriendsOfFriends(friends)
            })
        }
      }
      return getFriendsOfFriends(returnedFriends)
    })
}

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

Примечание - используйтеpromise.all()попытаться упроститьPromiseLoops()Функция заставляет себя вести себя как функция с совершенно другой функцией. Цель этого фрагмента — выполнять операции последовательно (одну за другой), ноPromise.allзаключается в одновременном выполнении всех асинхронных операций (всех сразу). Однако стоит подчеркнуть, что Async/await — это то же самое, что иPromise.all()Комбинации по-прежнему очень сильны, как мы покажем в следующем разделе.

Вторая попытка - цикл for с Async/Await

Это может быть очень просто.

async function asyncAwaitLoops () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)

  for (let friend of friends) {
    let moreFriends = await api.getFriends(friend.id)
    console.log('asyncAwaitLoops', moreFriends)
  }
}

Не нужно писать никаких рекурсивных промисов, просто цикл for. Видишь, это твой друг по жизни — Async/Await

ПАРАЛЛЕЛЬНЫЕ ОПЕРАЦИИ

Получение каждого списка друзей по одному кажется немного медленным, почему бы не делать это параллельно? Можем ли мы использовать async/await для выполнения этого требования?

Очевидно, да. Ваш друг может решить любую проблему. :)

async function asyncAwaitLoopsParallel () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)
  const friendPromises = friends.map(friend => api.getFriends(friend.id))
  const moreFriends = await Promise.all(friendPromises)
  console.log('asyncAwaitLoopsParallel', moreFriends)
}

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

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

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

Вернемся к предыдущему примеру и добавим обработку ошибок для каждой попытки. Мы будем использовать дополнительную функцию перед получением изображения пользователя.api.throwError()для обнаружения обработки ошибок.

Первая попытка - функция обратного вызова ошибки обещания

Давайте посмотрим на худший способ написать это:

function callbackErrorHell () {
  const api = new Api()
  let user, friends
  api.getUser().then(function (returnedUser) {
    user = returnedUser
    api.getFriends(user.id).then(function (returnedFriends) {
      friends = returnedFriends
      api.throwError().then(function () {
        console.log('Error was not thrown')
        api.getPhoto(user.id).then(function (photo) {
          console.log('callbackErrorHell', { user, friends, photo })
        }, function (err) {
          console.error(err)
        })
      }, function (err) {
        console.error(err)
      })
    }, function (err) {
      console.error(err)
    })
  }, function (err) {
    console.error(err)
  })
}

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

Вторая попытка - цепной метод захвата

Мы можем улучшить это, используя комбинацию обещания и поимки (обещание, поимка, обещание, поимка снова).

function callbackErrorPromiseChain () {
  const api = new Api()
  let user, friends
  api.getUser()
    .then((returnedUser) => {
      user = returnedUser
      return api.getFriends(user.id)
    })
    .then((returnedFriends) => {
      friends = returnedFriends
      return api.throwError()
    })
    .then(() => {
      console.log('Error was not thrown')
      return api.getPhoto(user.id)
    })
    .then((photo) => {
      console.log('callbackErrorPromiseChain', { user, friends, photo })
    })
    .catch((err) => {
      console.error(err)
    })
}

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

Третья попытка - обычный блок try/catch

Мы можем сделать лучше.

async function aysncAwaitTryCatch () {
  try {
    const api = new Api()
    const user = await api.getUser()
    const friends = await api.getFriends(user.id)

    await api.throwError()
    console.log('Error was not thrown')

    const photo = await api.getPhoto(user.id)
    console.log('async/await', { user, friends, photo })
  } catch (err) {
    console.error(err)
  }
}

Здесь мы заключаем всю операцию в обычный блок try/catch. Таким образом, мы можем одинаково выдавать и перехватывать ошибки из синхронного и одношагового кода. Понятно, что проще ;)

Сочинение

Я упоминал ранее, что любойasyncФункция метки фактически возвращает объект обещания. Это позволяет нам очень легко составить асинхронный поток управления.

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

async function getUserInfo () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)
  const photo = await api.getPhoto(user.id)
  return { user, friends, photo }
}

function promiseUserInfo () {
  getUserInfo().then(({ user, friends, photo }) => {
    console.log('promiseUserInfo', { user, friends, photo })
  })
}

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

async function awaitUserInfo () {
  const { user, friends, photo } = await getUserInfo()
  console.log('awaitUserInfo', { user, friends, photo })
}

Что, если нам теперь нужно получить все данные для первых десяти пользователей?

async function getLotsOfUserData () {
  const users = []
  while (users.length < 10) {
    users.push(await getUserInfo())
  }
  console.log('getLotsOfUserData', users)
}

Как насчет того, когда требуется параллелизм? Как насчет строгой обработки ошибок?

async function getLotsOfUserDataFaster () {
  try {
    const userPromises = Array(10).fill(getUserInfo())
    const users = await Promise.all(userPromises)
    console.log('getLotsOfUserDataFaster', users)
  } catch (err) {
    console.error(err)
  }
}

Заключение

С появлением одностраничных веб-приложений JavaScript и широким распространением NodeJS изящная обработка параллелизма важна как никогда для разработчиков JavaScript. Async/Await устраняет множество проблем с потоком управления, изобилующих ошибками, которые десятилетиями преследовали кодовые базы JavaScript, и в значительной степени гарантирует, что любой асинхронный блок кода будет чище, проще и увереннее. А недавно async/await полностью поддерживался почти во всех основных браузерах и nodejs, поэтому сейчас самое подходящее время для интеграции этих технологий в вашу практику написания кода и проекты.

время обсуждения

Присоединяйтесь к обсуждению на Reddit

async/await делает ваш код проще1

async/await делает ваш код проще2