Эволюция асинхронного программирования в JavaScript

Tags: JavaScript

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

Callback Hell

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

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

step1(function (error, result1) {

if (error) throw error

step2(result1, function (error, result2) {

  if (error) throw error

  step3(result2, function (error, result3) {

    if (error) throw error

    step4(result3, function (error, result4) {

      if (error) throw error

      console.log(result4)

    })

  })

})

})

Обещания

Первым достижением стало решение библиотеки: обещания (promises). Вместо принятия обратного вызова асинхронная функция возвращает объект обещания. В конечном итоге обещание либо разрешается со значением, либо отклоняется с ошибкой. В любом случае, оно завершается. Чтобы узнать, когда это произойдет, вы можете прикрепить обратные вызовы success и / или fail с методами then и catch соответственно. Эти методы возвращают новое обещание, к которому могут быть присоединены дополнительные обратные вызовы. Если обратный вызов возвращает значение, оно передается следующему, а затем обратному вызову в цепочке. Если он генерирует исключение, он передается следующему обратному вызову catch. Если он возвращает обещание, он вставляется в этот момент в цепочку. Обещания обеспечили облегчение, заменив вложенные обратные вызовы связанными обратными вызовами, которые обычно считаются более простыми для чтения:

step1()

.then(result1 => step2(result1))

.then(result2 => step3(result2))

.then(result3 => step4(result3))

.then(result4 => console.log(result4))

.catch(error => throw error)

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

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

Обещания идут со своими проблемами. Мы по-прежнему используем обратные вызовы, они просто связаны, а не вложены. Самый популярный способ поделиться одним асинхронным результатом между несколькими обратными вызовами - это сохранить его в переменной, хранящейся вне обратных вызовов. Исключения обрабатываются с помощью обратного вызова вместо привычной структуры try-catch. По сравнению с синхронным кодом он еще менее читабелен.

Генераторы

Функции генератора - это функции JavaScript, которые могут выдавать несколько значений перед окончательным возвратом. Они определены в JavaScript со специальным синтаксисом function*:



function* g() {

yield 1

yield 2

return (yield 3) + 1

}

Вызов функции генератора не запускает выполнение функции. Вместо этого он возвращает объект генератора, который представляет вызов функции. Вы можете пройти через функцию, вызвав ее метод next. При первом вызове next выполняется функция с начала до первого выхода, возвращая значение, которое было получено. ² При втором вызове next он снова входит в функцию в том месте, где он был получен, заменяет выход выражение с любым аргументом, который вы передали next, и продолжается до следующего yield. Этот процесс повторяется каждый раз, когда вы вызываете next, пока функция, наконец, не вернет или не сгенерирует (REPL):

const generator = g()

generator.next() // { value: 1, done: false }

generator.next() // { value: 2, done: false }

generator.next() // { value: 3, done: false }

generator.next(10) // { value: 11, done: true }

Вместо  next вы можете вызвать throw, чтобы вызвать исключение из выражения yield. Блок try-catch внутри функции генератора может его перехватить, но если он не обработан, он передаст стек вызовов в область, в которой вы вызвали throw (REPL):

function* g() {

try {

  yield 1 // throws 'first error'

} catch (error) { // catches 'first error'

  console.error('inside generator', error)

}

yield 2 // throws but does not catch 'second error'

return 3 // never reached because exception thrown

}



const generator = g()

generator.next() // { value: 1, done: false }

generator.throw('first error') // { value: 2, done: false }

try {

generator.throw('second error') // exception escapes generator

} catch (error) { // catches 'second error'

console.error('outside generator', error)

}

generator.next() // { value: undefined, done: true }

Есть несколько связанных терминов, с которыми вы можете столкнуться при изучении генераторов. То, как пошаговый код (путем вызова методов next и throw) и генератора (с yield, throw и return) передают управление между собой, называется кооперативной многозадачностью. Это «подпрыгивание» - вот почему ступенчатый код называется батутом. Наконец, генераторы - это тип сопрограммы.

Представьте себе, если мы напишем генератор, который дает обещания. Мы можем связать его со специальным батутом, который перехватывает каждое полученное обещание и добавляет обратные вызовы успеха и неудачи, которые передают свои аргументы обратно генератору, вызывая next или throw соответственно. Батут сам по себе возвращает обещание, которое разрешается с помощью возвращаемого значения генератора (или отклоняется с единственным неперехваченным исключением). Это позволяет нам писать асинхронный код синхронным способом:



const getFullName = async(function* (username, password) {

let token

try {

  token = yield logIn(username, password);

} catch (error) {

  console.error('wrong username or password')

  return

}

const user = yield getUser(token)

return user.fullName

})

Асинхронные функции

Этот шаблон оказался настолько популярным, что он был закреплен в языке с нативным синтаксисом в качестве асинхронных функций. Единственное отличие состоит в том, что async (function * (...) {...}) становится async function (...) {...}, и yield становится ожидаемым:

 

async function getFullName(username, password) {

let token

try {

  token = await logIn(username, password);

} catch (error) {

  console.error('wrong username or password')

  return

}

const user = await getUser(token)

return user.fullName

})

Сноски

¹ На самом деле, then принимает как успешные, так и неудачные обратные вызовы, но и то, и другое опционально. catch принимает только обратный вызов сбоя и аналогичен вызову без успешного обратного вызова.

 

² На самом деле next возвращает структуру типа {value: any, done: bool}. Поле done указывает, прибывает ли значение из оператора return (true) или выражения yield (false).

No Comments

Add a Comment