Почему эта асинхронная / ожидающая функция возвращает обещание? - PullRequest
0 голосов
/ 25 сентября 2019

Я не уверен, почему следующая функция возвращает обещание, когда я использую операторы async / await?

getBase64 = async (url) => {
  const response = await axios.get(url, {
  responseType: 'arraybuffer'
  })

  const buffer = new Buffer.from(response.data,'binary').toString('base64')
  return ('data:image/jpeg;base64,' + buffer)
}

Я знаю, что могу просто добавить .then(data => console.log(data)), ноЯ хочу присвоить необработанные данные переменной, например:

const base64Img = getBase64()

... что невозможно, так как возвращаемый тип является обещанием по какой-то причине.

Ответы [ 2 ]

2 голосов
/ 25 сентября 2019

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

const bas64Img = await getBase64()

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

1 голос
/ 26 сентября 2019

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

Это объяснение их выбора, которое я попытаюсь объяснить, но я уже могу вам сказать, что оно связано с некоторыми другими соответствующими частями спецификации языка, такими как контексты выполнения , заданий иочереди работ .Эта спецификация была введена частично из-за исторических причин - ECMAScript возник в виде JavaScript в веб-браузерах и того, как они позволяли создавать сценарии для веб-страниц.Поскольку язык был стандартизирован после того, как некоторое время было реализовано несколько реализаций, ECMAScript был определен задним числом, частично.В любом случае, что особенно важно, ECMAScript не допускает параллельного выполнения скрипта - например, вызов функции JavaScript никогда не будет прерван никаким другим вызовом функции, точка.Не в одной и той же области (в веб-браузерах одна область связана с одной визуализированной веб-страницей).

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

Рассматривая тело getBase64, давайте предположим, что axios.get(...) - потенциально длительная операция, потому что онавыбирает ресурс в сети.Если предположить, что это не так, getBase64 не обязательно должен быть async, не может использовать await и ведет себя как обычная функция.Итак, ради этого ответа мы предполагаем, что он включает в себя длительную операцию, и у нас нет значения для response до завершения этой длительной операции.axios.get может извлекать ресурс по сети, но характер операции на самом деле не имеет значения, важно то, что в противном случае операция может занять больше времени, чем выполнение всего сценария в противном случае.

Итак, можем ли мы продолжить выполнение тела getBase64 и вернуться до завершения axios.get(...)?Ради аргумента, мы могли бы выполнить некоторые его части, которые не зависят от обещания, возвращенного разрешением axios.get(...).Это называется автоматическим распараллеливанием кода.ECMAScript не определяет это и может даже не допустить.Мало языков делают.Но, как я уже говорил, технически возможно оценить те утверждения в теле функции, которые не зависят от разрешенного обещания, но в любом случае возвращаемое значение getBase64 зависит от response, косвенно, поэтому мыменьше всего нужно длительной операции, чтобы завершить, прежде чем вернуться.Это важная вещь.

Так что, если мы не можем вернуться еще, мы должны дождаться выполнения обещания, возвращенного axios.get(...).Я имею в виду, так или иначе нам нужно иметь response, прежде чем возвращать значение из getBase64.

Вот где это становится интересным.Среда выполнения сценариев всегда имеет среду, в которой на самом деле находятся сценарии.Частью этой среды может быть окно браузера (window), веб-страница (document) или что-то еще целиком.В качестве заметного примера совместимой среды выполнения ECMAScript, Node.js также имеет среду.Тогда возникает вопрос: что делать, если событие должно быть отправлено средой, пока getBase64 ожидает разрешения axios.get(...)?Может быть входящий сетевой запрос (Node.js) или пользователь щелкает ссылку (веб-браузер).К вам также подключен прослушиватель событий (не имеет смысла иметь события, на которые вы не можете реагировать, не так ли?).Теперь вы вызываете и начинаете выполнять прослушиватель событий, пока вызов getBase64 ожидает response, или вы ожидаете завершения getBase64 и остальной части сценария (представьте цепочку вызовов) для завершения,рискуя плохим пользовательским опытом из-за того, что пользователь ожидает потенциально «долгое» время (например, сеть «медленная»)?

Если вы начнете выполнять обработчик событий, вы неизбежно получите два контекста выполнения скрипта впараллельный - выполнение обработчика события и вызов getBase64, ожидающий разрешения axios.get(...).По сути, ваш обработчик событий выполняется до того, как возвращается getBase64, все еще в процессе, даже если агент использует только один поток выполнения, где первый опережает второй.Это все еще параллельное выполнение скрипта.Это немаловажно - поведение вашего скрипта, в частности порядок выполнения операторов, теперь зависит от времени события!Вам придется иметь дело с потенциальными условиями гонки и проблемами с синхронизацией состояний в целом.

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

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

НекоторыеМожно назвать это чрезмерным уточнением языка, но я считаю, что я сделал для этого какое-то обоснование.В любом случае, у JavaScript были гораздо более скромные амбиции, прежде чем он был стандартизирован, и относительная простота написания сценариев достижима, если избегать ситуаций, возникающих в результате параллельного выполнения сценариев, и в результате упрощать архитектуру веб-браузера в то время, когда никто не говорил о ЦП.«основные», были приоритетами.ECMAScript стандартизировал это поведение с жестко определенной моделью.

Это, конечно, все еще оставляет нам проблему "замораживания" страниц, когда выполнение сценария занимает слишком много времени, поскольку выполнение обработчиков событий и всего, что влияет на среду, подверженную воздействию сценариев, должно быть отложено до завершения текущего выполнения.И если вы когда-нибудь пытались вычислить что-нибудь достаточно сложное в ваших скриптах страниц, вы понимаете, что я имею в виду.Два параллельных выполнения сценариев не выполняются.Выполнение обратных вызовов для интервалов, таймеров, обещаний и других операций ставится в очередь и выполняется в их собственных контекстах выполнения, как и выполнение сценария.Эти и другие задания - фактическая последовательность сетевых запросов и операций, инициированных различными API-интерфейсами, - используют очереди заданий ECMAScript.Кроме того, HTML определяет что-то, называемое цикл событий , который также использует очереди заданий для внутреннего использования.Эти спецификации жестко определяют, как обрабатываются события, когда обратные вызовы запускаются различными API-интерфейсами, и в остальном все еще позволяют среде сценариев реагировать между ожидаемыми кратковременными выполнениями сценариев.

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

Таким образом, в заключение да, технически мы могли бы иметь async функции, которые всегда возвращаютзначения, которые вы им сообщаете, должны возвращаться с ключевым словом return, но вы будете торговать большой простотой разработки JavaScript в обмен на другой вид «простоты», допускающий прерывания функций async (чередование контекстов выполнения вв общем) для того, чтобы замаскировать обработку обратных вызовов / обещаний и иметь возможность напрямую работать с возвращаемыми значениями, но при этом необходимо более внимательно наблюдать за изменениями состояния и постоянно опасаться особенностей времени выполнения.Дизайнеры ECMAScript сделали выбор за вас.

Почему некоторые async функции не могут возвращать "фактические" значения?

Даже без разрешения чередования контекста выполнения,технически ничто не помешает функции async, которая не использует await, возвращать значение, которое вы намеревались.Функция сможет выполняться до завершения, не ожидая каких-либо обещаний.По существу, тогда он ведет себя как обычная функция, будучи async «только по имени».Но вы бы нарушили спецификацию ECMAScript, которая обязывает асинхронные функции всегда возвращать обещание.Опять же, решение дизайнеров спецификации ECMAScript.

Причина их решения мне не известна, но я осмелюсь возразить, что так не нужно смотреть на реализацию async функция («есть ли здесь await ключевые слова?»), Чтобы узнать, как ее использовать - можно использовать такой же код (например, снова используя await) для вызова функции и обработки результатапросто зная, если это async или нет.Если это так, вы будете использовать await foo() или foo().then(result => { ... }), где foo - такая функция async, и она всегда будет работать независимо от того, включает ли само выполнение foo await или нет (async function foo() { return 1; }).Вам также не нужно смотреть на его реализацию.Я думаю, что это называется «контрактом вызова».

Почему нельзя использовать await без async?

По аналогии с объяснением, приведенным выше, решение о назначении функций должно быть помечено как async, чтобы иметь возможность использовать await, опять же, выбор со стороны разработчиков ECMAScript.ECMAScript может позволить любой функции использовать await, но тогда, поскольку язык не перемежает контексты выполнения, любая такая функция должна будет возвращать обещание, иначе это серьезно повлияет на обработку событий.Если исходить из первого и придерживаться этой мысли, то либо все функции будут возвращать обещания, либо только те, которые используют await. Все Функции JavaScript, всегда возвращающие обещания, вероятно, будут, по крайней мере, признаны разработчиками ECMAScript ужасной идеей, и кажется, что вы сами выражаете мнение по поводу своего вопроса.Наличие только тех функций, которые используют await, всегда возвращает обещания, в то время как лучшая идея будет означать, что тип значения, возвращаемого функцией , меняется в зависимости от реализации функции , что-то вроде мухи в лицопринципа "вызова контрактов", опять же - вам нужно знать, использует ли foo await, чтобы знать, как работать с результатом вызова этого foo.Разработчики ECMAScript предпочли против этого, как мне кажется, ожидаемо, и именно поэтому функции, которые используют await, должны быть явно помечены - с помощью async.

Кроме того, обе спецификации, вероятно, помогут сделать для более оптимизированного языкавремя выполнения.

1 Я использую «ECMAScript» и «JavaScript» взаимозаменяемо, хотя допускаю, что могут быть различия.

...