Используйте Async/Await для написания лаконичного асинхронного кода.

Node.js внешний интерфейс JavaScript Promise
Используйте Async/Await для написания лаконичного асинхронного кода.
原文链接:https://blog.patricktriest.com/what-is-async-await-why-should-you-care/

Прекратите писать обратные вызовы и влюбитесь в ES8

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

задний план

AJAX

Давайте сначала обратимся к истории. В конце 1990-х Ajax стал первым крупным прорывом в асинхронном JavaScript. Эта технология позволяет веб-сайтам извлекать и отображать последние данные после загрузки HTML, что является революционной идеей. До этого большинство сайтов загружали всю страницу заново, чтобы отобразить обновленный контент. Эта технология (популярная под названием ajax в jQuery) доминировала в веб-разработке с 2000 по 2010 год, и в настоящее время Ajax является основной технологией, используемой веб-сайтами для извлечения данных, но XML в значительной степени заменил JSON.

NodeJS

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

Promises

Несколько лет спустя в среде NodeJS и браузерах появился новый стандарт под названием «Промисы». Промисы предоставляют мощный стандартизированный способ составления асинхронных операций. Обещания по-прежнему используют формат, основанный на обратном вызове, но предоставляют согласованный синтаксис для объединения и составления асинхронных операций. В 2015 году обещания, поддерживаемые популярной библиотекой с открытым исходным кодом, наконец были добавлены в качестве встроенной функции JavaScript. Промисы — хорошее улучшение, но они по-прежнему часто являются причиной некоторых длинных, трудночитаемых блоков кода. И теперь есть решение. Async/Await — это новый синтаксис (заимствованный из .net и C#), который позволяет нам писать промисы, но они выглядят как синхронный код без обратных вызовов и могут использоваться для упрощения практически любого существующего JS-приложения. Async/Await — это новая функция языка JavaScript, официально добавленная как нативная функция JavaScript в ES7.

Пример

Мы покажем магию асинхронизации / ждут некоторых примеров кода

Примечание: для запуска приведенных ниже примеров библиотека не требуется. Async/Await полностью поддерживается последними версиями Chrome, FireFox, Safari, Edge, и вы можете запускать примеры в консоли браузера. Async/Await должен работать на NodeJS 7.6 и выше, а также поддерживается транспиляторами Babel и TypeScript. Таким образом, Async/Await можно использовать в реальной разработке.

Подготовить

Мы будем использовать фиктивный класс 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)
    })
  }
}

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

Метод 1 --- Функции обратного вызова с вложенными промисами

Использование вложенных обратных вызовов промисов

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, этот блок кода хорошо знаком. Очень простая бизнес-логика, но код длинный, глубоко вложенный и в итоге.....

     })
    })
  })
}

В реальном бизнес-сценарии каждая функция обратного вызова может быть более сложной, а блок кода будет представлять собой набор иерархических})на конец. «Функции обратного вызова вложены в функции обратного вызова, а функции обратного вызова вложены», что является легендарным «адом обратных вызовов» («ад обратных вызовов» родился не только из-за путаницы блоков кода, но и из-за проблем с доверием). Что еще хуже, мы не сделали механизм обработки ошибок для упрощения.Если добавлено отклонение...

Способ 2 --- Цепочка обещаний

давайте будем элегантными

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.prototype.then() и Promise.prototype.catch() возвращают объекты Promise, что позволяет нам соединить эти промисы в цепочку промисов. Таким образом, мы можем разместить эти функции обратного вызова в иерархии с отступом. В то же время мы используем стрелочные функции для упрощения объявления функции обратного вызова. По сравнению с предыдущим адом обратных вызовов, использование цепочек промисов делает код более читаемым и имеет лучшее чувство последовательности, но все равно выглядит очень многословным и немного сложным.

Метод 3 --- Асинхронный/ожидание

Можем ли мы не написать функцию обратного вызова? Можно ли решить ее всего с помощью 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 })
}

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

Примечание: await необходимо использовать с async.Обратите внимание, что в приведенной выше функции мы поместили ключевое слово async перед объявлением функции, что является обязательным. Мы подробно обсудим это позже

цикл

Async/Await может сделать большой объем сложного кода в прошлом кратким. Например, если мы хотим получить список друзей каждого пользователя по порядку.

Метод 1 --- рекурсивный цикл промисов

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

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)
    })
}

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

Метод 2 --- Async/Await For-Loop

Давайте попробуем 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 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(), Promise.all() возвращает обещание, после того как все запросы будут завершены разрешением.

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

Однако в асинхронном программировании есть большая нерешенная проблема: обработка ошибок. В асинхронных операциях мы должны писать отдельные обратные вызовы для обработки ошибок для каждой операции, и поиск правильного местоположения ошибки в верхней части стека вызовов может быть сложным, поэтому мы должны проверять в начале каждого обратного вызова, чтобы увидеть, не выдает ли он ошибку. . Таким образом, функция обратного вызова после введения обработки ошибок будет экспоненциально сложнее, чем раньше, и если местоположение отчета об ошибке не находится активно, эти ошибки будут даже «проглатывать». Теперь давайте добавим обработку ошибок к предыдущему примеру. Чтобы протестировать механизм обработки ошибок, мы будем использовать абстрактный классapi.throwError()метод.

Метод 1 --- обратные вызовы ошибок обещания

Давайте рассмотрим худший случай

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)
  })
}

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

Метод 2 --- Цепочка обещаний Метод "Catch"

Мы можем добавить в цепочку обещанийcatchспособы улучшить некоторые

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.

Метод 3 --- Обычный блок 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 в функции получения данных, что сделает весь асинхронный модуль более очевидным. Что, если мы хотим получить данные первых 10 пользователей?

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)
  }
}

В заключение

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