Освоение Async и Await в Node.js

Node.js

В этой статье вы узнаете, как использовать асинхронные функции (async/await) в Node.js для упрощения обратных вызовов или промисов.

Структуры асинхронных языков уже существуют в других языках, таких как async/await в C#, сопрограммы в Kotlin и горутины в go.С выпуском Node.js 8 долгожданная асинхронная функция также реализована по умолчанию.

Что такое асинхронная функция в Node?

Когда функция объявлена ​​как асинхронная, она возвращаетAsyncFunctionпредметы, похожие наGeneratorПотому что исполнение может быть приостановлено. Разница лишь в том, что они возвращаютсяPromiseвместо{ value: any, done: Boolean }объект. Они все еще очень похожи, вы можете использоватьcopackage для получения той же функциональности.

В асинхронной функции вы можете ждатьPromiseЗаполните или зафиксируйте причину отклонения.

Если вы хотите реализовать свою собственную логику в Promises

function handler (req, res) {
  return request('https://user-handler-service')
    .catch((err) => {
      logger.error('Http error', err)
      error.logged = true
      throw err
    })
    .then((response) => Mongo.findOne({ user: response.body.user }))
    .catch((err) => {
      !error.logged && logger.error('Mongo error', err)
      error.logged = true
      throw err
    })
    .then((document) => executeLogic(req, res, document))
    .catch((err) => {
      !error.logged && console.error(err)
      res.status(500).send()
    })
}

можно использоватьasync/awaitСделайте этот код похожим на синхронно выполняемый код

async function handler (req, res) {
  let response
  try {
    response = await request('https://user-handler-service')  
  } catch (err) {
    logger.error('Http error', err)
    return res.status(500).send()
  }

  let document
  try {
    document = await Mongo.findOne({ user: response.body.user })
  } catch (err) {
    logger.error('Mongo error', err)
    return res.status(500).send()
  }

  executeLogic(document, req, res)
}

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

process.on('unhandledRejection', (err) => { 
  console.error(err)
  process.exit(1)
})

шаблон асинхронной функции

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

Используйте данные асинхронной выборки, когда вам нужно переработать или использоватьif-elseУсловное время — очень сложная ситуация.

Экспоненциальный механизм отсрочки

использоватьPromiseРеализация резервной логики довольно неуклюжая.

function requestWithRetry (url, retryCount) {
  if (retryCount) {
    return new Promise((resolve, reject) => {
      const timeout = Math.pow(2, retryCount)
 
      setTimeout(() => {
        console.log('Waiting', timeout, 'ms')
        _requestWithRetry(url, retryCount)
          .then(resolve)
          .catch(reject)
      }, timeout)
    })
  } else {
    return _requestWithRetry(url, 0)
  }
}

function _requestWithRetry (url, retryCount) {
  return request(url, retryCount)
    .catch((err) => {
      if (err.statusCode && err.statusCode >= 500) {
        console.log('Retrying', err.message, retryCount)
        return requestWithRetry(url, ++retryCount)
      }
      throw err
    })
}

requestWithRetry('http://localhost:3000')
  .then((res) => {
    console.log(res)
  })
  .catch(err => {
    console.error(err)
  })

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

function wait (timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, timeout)
  })
}

async function requestWithRetry (url) {
  const MAX_RETRIES = 10
  for (let i = 0; i <= MAX_RETRIES; i++) {
    try {
      return await request(url)
    } catch (err) {
      const timeout = Math.pow(2, i)
      console.log('Waiting', timeout, 'ms')
      await wait(timeout)
      console.log('Retrying', err.message, i)
    }
  }
}

Приведенный выше код выглядит очень удобно, не так ли?

медиана

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

functionAвернутьPromise,ТакfunctionBтребует это значение иfunctioinCнужноfunctionAа такжеfunctionBЗавершенное значение.

план 1:thenРождественская елка

function executeAsyncTask () {
  return functionA()
    .then((valueA) => {
      return functionB(valueA)
        .then((valueB) => {          
          return functionC(valueA, valueB)
        })
    })
}

С этим решением мы в третьемthenдоступно вvalueAа такжеvalueB, то можно перейти к первым двумthenполучить то же самоеvalueAа такжеvalueBценность . Здесь нет способа сплющить елку (испортить к черту), вы потеряете замыкание, если вы это сделаете,valueAсуществуетfunctioinCОн будет недоступен.

Сценарий 2: переход к предыдущей области

function executeAsyncTask () {
  let valueA
  return functionA()
    .then((v) => {
      valueA = v
      return functionB(valueA)
    })
    .then((valueB) => {
      return functionC(valueA, valueB)
    })
}

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

Это очень эффективное сглаживание.thenchain "правильный" синтаксис, однако в этом методе нам нужно использовать две переменныеvalueAа такжеvдля хранения одного и того же значения.

Вариант 3. Используйте избыточный массив

function executeAsyncTask () {
  return functionA()
    .then(valueA => {
      return Promise.all([valueA, functionB(valueA)])
    })
    .then(([valueA, valueB]) => {
      return functionC(valueA, valueB)
    })
}

в функцииfunctionAизthenиспользуя массив вvalueAа такжеPromiseВернитесь вместе, что эффективно сплющит рождественскую елку (ад обратного вызова).

Вариант 4: Напишите вспомогательную функцию

const converge = (...promises) => (...args) => {
  let [head, ...tail] = promises
  if (tail.length) {
    return head(...args)
      .then((value) => converge(...tail)(...args.concat([value])))
  } else {
    return head(...args)
  }
}

functionA(2)
  .then((valueA) => converge(functionB, functionC)(valueA))

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

использоватьasync/awaitНаши проблемы чудесным образом исчезли

async function executeAsyncTask () {
  const valueA = await functionA()
  const valueB = await functionB(valueA)
  return function3(valueA, valueB)
}

использоватьasync/awaitОбработка нескольких параллельных запросов

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

async function executeParallelAsyncTasks () {
  const [ valueA, valueB, valueC ] = await Promise.all([ functionA(), functionB(), functionC() ])
  doSomethingWith(valueA)
  doSomethingElseWith(valueB)
  doAnotherThingWith(valueC)
}

Метод итерации массива

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

1.map
function asyncThing (value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), 100)
  })
}

async function main () {
  return [1,2,3,4].map(async (value) => {
    const v = await asyncThing(value)
    return v * 2
  })
}

main()
  .then(v => console.log(v))
  .catch(err => console.error(err))
2.filter
function asyncThing (value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), 100)
  })
}

async function main () {
  return [1,2,3,4].filter(async (value) => {
    const v = await asyncThing(value)
    return v % 2 === 0
  })
}

main()
  .then(v => console.log(v))
  .catch(err => console.error(err))
3.reduce

function asyncThing (value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), 100)
  })
}

async function main () {
  return [1,2,3,4].reduce(async (acc, value) => {
    return await acc + await asyncThing(value)
  }, Promise.resolve(0))
}

main()
  .then(v => console.log(v))
  .catch(err => console.error(err))
решение:
  1. [ Promise { <pending> }, Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]

  2. [ 1, 2, 3, 4 ]

  3. 10

Если это данные итерации карты, вы увидите, что возвращаемое значение равно[ 2, 4, 6, 8 ], единственная проблема заключается в том, что каждое значениеAsyncFunctionфункция, завернутая вPromiseсередина

Поэтому, если вы хотите получить их значения, вам нужно передать массив вPromise.All()распутатьPromiseупаковка.

main()
  .then(v => Promise.all(v))
  .then(v => console.log(v))
  .catch(err => console.error(err))

Сначала ты будешь ждатьPromiseРешите, затем используйте карту для перебора каждого значения

function main () {
  return Promise.all([1,2,3,4].map((value) => asyncThing(value)))
}

main()
  .then(values => values.map((value) => value * 2))
  .then(v => console.log(v))
  .catch(err => console.error(err))

Это вроде проще?

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

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

оfilterЭта проблема

Возможно, вы заметили, что даже если приведенная выше функция фильтра возвращает[ false, true, false, true ],await asyncThing(value)вернетpromiseТогда вы обязательно получите примитивное значение. Вы можете дождаться завершения всех асинхронных операций перед возвратом во время фильтрации.

ReducingОчень просто, стоит отметить, что вам нужно обернуть начальное значение вPromise.resolveсередина

Перепишите приложение узла на основе обратного вызова, чтобы

AsyncПо умолчанию функция возвращаетPromise, так что вы можете использоватьPromisesпереопределить любой на основеcallbackфункция, тоawaitПодождите, пока они закончат выполнение. также может использоваться в узлеutil.promisifyпреобразует функцию обратного вызова вPromiseФункция

Перепишите приложение на основе Promise

Это легко преобразовать,.thenСвязать поток выполнения Promise. Теперь вы можете использовать `async/await напрямую.

function asyncTask () {
  return functionA()
    .then((valueA) => functionB(valueA))
    .then((valueB) => functionC(valueB))
    .then((valueC) => functionD(valueC))
    .catch((err) => logger.error(err))
}
 

после преобразования

async function asyncTask () {
  try {
    const valueA = await functionA()
    const valueB = await functionB(valueA)
    const valueC = await functionC(valueB)
    return await functionD(valueC)
  } catch (err) {
    logger.error(err)
  }
}
Rewriting Nod

использоватьAsync/AwaitЭто значительно сделает приложение более читабельным и уменьшит сложность обработки приложения (например, захват ошибок).Если вы также используете версию узла v8+, вы можете попробовать ее, и могут быть новые преимущества.

Если есть ошибка, пожалуйста, оставьте сообщение и позвольте мне исправить его, спасибо за чтение

Оригинальная ссылка