Как я могу использовать async/await на верхнем уровне?

Вопрос или проблема

Я изучал async/await и, прочитав несколько статей, решил протестировать это самостоятельно. Однако я не могу понять, почему это не работает:

async function main() {  
    var value = await Promise.resolve('Привет');
    console.log('внутри: ' + value);
    return value;
}

var text = main();  
console.log('снаружи: ' + text);

Консоль выводит следующее (node v8.6.0):

> снаружи: [object Promise]

> внутри: Привет

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

Есть ли способ, с помощью которого я мог бы использовать значение, возвращаемое внутри функции, без использования .then() после main()?

Я не могу понять, почему это не работает.

Потому что main возвращает промис; все async функции так делают.

На верхнем уровне вам нужно:

  1. Использовать top-level await (предложение, MDN; ES2022, широко поддерживается в современных средах), который позволяет использовать await на верхнем уровне в модуле.

    или

  2. Использовать верхнеуровневую async функцию, которая никогда не отказывается (если вы не хотите ошибок “необработанного отклонения”).

    или

  3. Использовать then и catch.

#1 top-level await в модуле

Вы можете использовать await на верхнем уровне модуля. Ваш модуль не завершит загрузку, пока промис, который вы await, не выполнится (это означает, что любой модуль, ожидающий загрузки вашего модуля, не завершит загрузку, пока промис не станет выполненным). Если промис отклоняется, ваш модуль не загрузится. Обычно верхнеуровневый await используется в ситуациях, когда ваш модуль не сможет выполнить свою работу, пока промис не будет разрешен, и не сможет сделать это вообще, если промис не будет выполнен, так что это нормально:

const text = await main();
console.log(text);

Если ваш модуль может продолжать работать, даже если промис отклоняется, вы могли бы обернуть верхнеуровневый await в try/catch:

// В модуле, как только предложение о верхнем уровне `await` будет принято
try {
    const text = await main();
    console.log(text);
} catch (e) {
    // Обработка факта, что цепочка не удалась
}
// `text` здесь не доступен

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

Вы не можете использовать await на верхнем уровне в не модульном скрипте, только в модулях.

#2 – Верхнеуровневая async функция, которая никогда не отказывает

(async () => {
    try {
        const text = await main();
        console.log(text);
    } catch (e) {
        // Обработка факта, что цепочка не удалась
    }
    // `text` здесь тоже не доступен, и код здесь выполняется до того, как промис выполнится
// и до выполнения кода после await в основной функции выше

Обратите внимание на catch; вы должны обрабатывать отклонения промисов / асинхронные исключения, так как никто другой этого не сделает; у вас нет вызывающего, которому их можно передать (в отличие от #1 выше, где вашим “вызывающим” является загрузчик модулей). Если хотите, вы можете сделать это на результате вызова через функцию catch (вместо синтаксиса try/catch):

(async () => {
    const text = await main();
    console.log(text);
})().catch(e => {
    // Обработка факта, что цепочка не удалась
});
// `text` здесь не доступен, и код здесь выполняется до того, как промис выполнится
// и до выполнения кода после await в основной функции выше

…что немного более кратко, хотя это несколько смешивает модели (async/await и явные колбеки промисов), что я обычно, в противном случае, советую не делать.

Или, конечно, не обрабатывать ошибки и просто позволить возникнуть ошибке “необработанного отклонения”.

#3 – then и catch

main()
    .then(text => {
        console.log(text);
    })
    .catch(err => {
        // Обработка факта, что цепочка не удалась
    });
// `text` здесь не доступен, и код здесь выполняется до того, как промис выполнится
// и обработчики выше запускаются

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

Или оба аргумента для then:

main().then(
    text => {
        console.log(text);
    },
    err => {
        // Обработка факта, что цепочка не удалась
    }
);
// `text` здесь не доступен, и код здесь выполняется до того, как промис выполнится
// и обработчики выше запускаются

Снова обратите внимание, что мы регистрируем обработчик отказов. Но в этой форме, убедитесь, что ни один из ваших колбеков then не выбрасывает ошибок, так как ничего не зарегистрировано для их обработки.

Ответ 2023: теперь вы можете использовать верхний уровень await во всех поддерживаемых версиях node.js

Большинство ответов выше немного устарели или очень многословны, поэтому вот краткий пример для node 14 и выше.

В JavaScript (без использования TypeScript)

Существует отдельный ответ для TypeScript ниже.

Создайте файл с именем runme.mjs:

import { exec as oldExec } from "node:child_process";
import { promisify } from "node:util";
const exec = promisify(oldExec);
const log = console.log.bind(console);

// Верхний уровень await теперь работает
const { stdout, stderr } = await exec("ls -la");
log("Вывод:\n", stdout);
log("\n\nОшибки:\n", stderr);

Запустите node runme.mjs

Вывод:
 всего 20
drwxr-xr-x  2 mike mike 4096 Авг 12 12:05 .
drwxr-xr-x 30 mike mike 4096 Авг 12 11:05 ..
-rw-r--r--  1 mike mike  130 Авг 12 12:01 file.json
-rw-r--r--  1 mike mike  770 Авг 12 12:12 runme.mjs

Ошибки:

Вам не нужно устанавливать type: module в ваш package.json, так как файлы .mjs предполагаются как ESM.

В TypeScript с использованием esrun

В TypeScript, используя esrun.

esrun лучше, чем ts-node, tsrun и tsx, потому что:

  • esrun не требует изменений в package.json
  • esrun не требует tsconfig.json
  • esrun никогда не говорит, что не знает, как запускать ts файлы
  • esrun не требует от вас использовать файлы .mts
  • esrun по умолчанию использует текущую версию JavaScript
npm i esrun

Просто запустите TS с npx esrun file.ts:

Вывод:
 всего 128
drwxr-xr-x@  8 mikemaccana  staff    256 18 Июл 13:50 .
drwxr-xr-x@ 13 mikemaccana  staff    416 17 Июл 14:05 ..
-rw-r--r--@  1 mikemaccana  staff     40 18 Июл 13:27 deleteme.ts
-rw-r--r--@  1 mikemaccana  staff    293 18 Июл 13:51 deletemetoo.ts
drwxr-xr-x@ 85 mikemaccana  staff   2720 18 Июл 13:51 node_modules
-rw-r--r--@  1 mikemaccana  staff  46952 18 Июл 13:51 package-lock.json
-rw-r--r--@  1 mikemaccana  staff    503 18 Июл 13:51 package.json
-rw-r--r--@  1 mikemaccana  staff   1788 18 Июл 13:48 utils.ts

⚠️ Вам не нужно устанавливать type: module в ваш package.json, так как esrun представляет собой разумные настройки по умолчанию.

Top-Level await перешел на этап 3 этап 4 (см. комментарий namo), так что ответ на ваш вопрос Как я могу использовать async/await на верхнем уровне? заключается в том, чтобы просто использовать await:

const text = await Promise.resolve('Привет');
console.log('снаружи: ' + text)

Или если вы хотите функцию main(): добавьте await к вызову main() :

async function main() {
    var value = await Promise.resolve('Привет');
    console.log('внутри: ' + value);
    return value;
}

var text = await main();  
console.log('снаружи: ' + text)

Совместимость

Чтобы дать дополнительную информацию в дополнение к текущим ответам:

Содержимое файла node.js в настоящее время конкатенируется, строкоподобным образом, чтобы сформировать тело функции.

Например, если у вас есть файл test.js:

// Удивительный тестовый файл!
console.log('Тест!');

Тогда node.js в тайне скомбинирует функцию, которая выглядит так:

function(require, __dirname /*, ... возможно, больше свойств верхнего уровня ... */) {
  // Удивительный тестовый файл!
  console.log('Тест!');
}

Главное, что нужно отметить, это то, что полученная функция НЕ является async функцией. Поэтому вы не можете использовать термин await непосредственно внутри нее!

Но скажем, вам нужно работать с промисами в этом файле, тогда есть два возможных метода:

  1. Не использовать await непосредственно внутри функции
  2. Совсем не использовать await

Вариант 1 требует от нас создать новую область видимости (и эта область может быть async, потому что мы контролируем это):

// Удивительный тестовый файл!
// Создайте новую асинхронную функцию (новую область видимости) и немедленно вызовите ее!
(async () => {
  await new Promise(...);
  console.log('Тест!');
})()
  .catch(err => console.log('Фатальная ошибка', err);

Вариант 2 требует от нас использовать объектно-ориентированный API промисов (менее красивую, но равнофункциональную парадигму работы с промисами)

// Удивительный тестовый файл!
// Создайте какой-то промис...
let myPromise = new Promise(...);

// Теперь используем объектно-ориентированный API
myPromise.then(() => console.log('Тест!'));

Интересно будет увидеть, как node добавляет поддержку верхнего уровня await!

Теперь вы можете использовать верхний уровень await в Node v13.3.0

import axios from "axios";

const { data } = await axios.get("https://api.namefake.com/");
console.log(data);

запустите с флагом --harmony-top-level-await

node --harmony-top-level-await index.js

Фактическое решение этой проблемы состоит в том, чтобы подойти к ней с другой стороны.

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

Решение заключается в том, чтобы убедиться, что на верхнем уровне вашего приложения всегда будет только одно единственное выражение JavaScript. Если у вас есть только одно выражение в верхней части вашего приложения, то вы вольны использовать async/await в любой другой точке повсюду (при обычных правилах синтаксиса)

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

Так должен выглядеть верхний уровень вашего приложения:

import {application} from './server'

application();

Мне нравится этот хитроумный синтаксис для выполнения асинхронной работы с точки входа

void async function main() {
  await doSomeWork()
  await doMoreWork()
}()

Для браузера вам нужно добавить type="module"

без type="module"

<script>
  const resp = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await resp.json();
  console.log(users)
</script>

с type="module"

<!--script type="module" src="https://stackoverflow.com/questions/79018089/await.js" -->
<script type="module">
  const resp = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await resp.json();
  console.log(users)
</script>

Другие решения не содержали некоторых важных деталей для соблюдения POSIX:

Вам нужно …

  • Сообщать 0 статус выхода при успехе и ненулевой при ошибке.
  • Выводить ошибки в поток вывода stderr.

Это обеспечит правильную интеграцию вашего командного приложения с другими командными инструментами в bash/zsh и даже Windows PowerShell или cmd. Например, если вы используете его как часть последовательности приложений, связанных через оператор конвейера |, то вывод ошибок в stderr обеспечивает их видимость (они не перенаправляются в следующую команду). Только stdout должен быть перенаправлен как stdin в следующую команду в вашем конвейере. Возвращение статуса выхода, соответствующего POSIX, означает, что вы можете корректно связывать ваше приложение Node с другими командами, используя оператор && или || в оболочке.

#!/usr/bin/env node

async function main() {
 // ... ожидаем вещи ... 
}

// Приложения, соответствующие POSIX, должны сообщать статус выхода
main()
  .then(() => {
    process.exit(0);
  })
  .catch(err => {
    console.error(err); // Запись в stderr
    process.exit(1);
  });

Если вы используете парсер командной строки, например commander, вам может не понадобиться main().

Пример:

#!/usr/bin/env node

import commander from 'commander'

const program = new commander.Command();

program
  .version("0.0.1")
  .command("some-cmd")
  .arguments("<my-arg1>")
  .action(async (arg1: string) => {
    // выполнить какое-то асинхронное действие
  });

program.parseAsync(process.argv)
  .then(() => {
    process.exit(0)
  })
  .catch(err => {
    console.error(err.message || err);
    if (err.stack) console.error(err.stack);
    process.exit(1);
  });

Node
Вы можете запустить node --experimental-repl-await в REPL. Я не сильно уверен по поводу скриптов.

Deno
Deno уже имеет это встроенным.

Теперь с ECMAScript22 мы можем использовать await на верхнем уровне модуля.

Это пример с ( await на верхнем уровне ):

const response = await fetch("...");
console.log(response):

другой пример без (await на верхнем уровне )

  async function callApi() {
    const response = await fetch("...");
    console.log(response)      
}
callApi()

const resp = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await resp.json();
console.log(users)

  1. Вам нужно добавить тип в package.json

    "type": "module"
    
  2. Вы готовы начать.

    import axios from 'axios';
    const res = await axios.get('https://api.github.com/users/wesbos');
    console.log(res.data);
    

Помните, если вы измените тип документа, то вам нужно будет писать код в стиле ES6.

Если ваша единственная цель – контролировать порядок выполнения асинхронного кода, смешанного с другим кодом для тестирования, вы можете обернуть весь верхний уровень кода в немедленно вызываемое функциональное выражение (IIFE), заданное как async функция. В примере из вопроса, вы затем добавите await перед вызовом main().

Вы можете использовать этот шаблон, когда ваш код еще не находится в async функции или на верхнем уровне тела модуля. Другими словами, если вы просто тестируете кучу кода внутри js-файла и используете инструменты, такие как Live Server, RunJs или любой другой тип JavaScript-песочницы, чтобы наблюдать за окном консоли, оберните весь ваш код в IIFE, заданное как async, и используйте ключевое слово await, когда хотите дождаться завершения асинхронного кода перед выполнением следующей строки.

let topLevelIIFE = (async () => {  
  async function main() {  
      var value = await Promise.resolve('Привет');
      console.log('внутри: ' + value);
      return value;
  }

  var text = await main();  
  console.log('снаружи: ' + text);

})()

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

В NodeJS 14.8+ вы можете использовать верхний уровень await модуль (#3 решение). Вы также можете переименовать .js в .mjs (ES модуль) вместо .js (.cjs CommonJS).

const ABC = ()=>{
return new Promise((resolve,reject)=>{
resolve()

})

ABC().then((res)=>{
// сделать что-то
})

}

Поскольку main() работает асинхронно, он возвращает промис. Вам нужно получить результат в методе then(). И поскольку then() также возвращает промис, вам нужно вызвать process.exit(), чтобы завершить программу.

main()
   .then(
      (text) => { console.log('снаружи: ' + text) },
      (err)  => { console.log(err) }
   )
   .then(() => { process.exit() } )

Ответ или решение

Чтобы использовать async/await на верхнем уровне (top-level) в JavaScript, вам нужно учитывать несколько основных моментов. Давайте разберем все шаги и примеры, чтобы вы могли использовать их в своих проектах, особенно если вы работаете с Node.js или современными браузерами.

1. Понимание async/await

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

2. Основная проблема

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

async function main() {  
    var value = await Promise.resolve('Привет');
    console.log('внутри: ' + value);
    return value;
}

var text = main();  
console.log('снаружи: ' + text);

Консоль выведет:

снаружи: [object Promise]
внутри: Привет

Это происходит потому, что main() возвращает промис, и console.log('снаружи: ' + text) выполняется прежде, чем тот промис будет разрешен.

3. Использование top-level await

Если вы используете Node.js версии 14.8 и выше или соответствующий браузер, вы можете использовать top-level await в модулях. Вот как это выглядит:

Пример с использованием top-level await

// В файле main.mjs
const value = await Promise.resolve('Привет');
console.log('снаружи: ' + value);

Важно, чтобы файл имел расширение .mjs, чтобы браузер или Node.js распознавал его как модуль. Эта конструкция позволит вам использовать await вне асинхронной функции.

4. Использование асинхронной функции

Если вы по какой-то причине не можете использовать top-level await, вы можете обернуть ваш код в асинхронную самовызывающуюся функцию. Например:

(async () => {
    const value = await main();
    console.log('снаружи: ' + value);
})();

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

При использовании await на верхнем уровне, необходимо обрабатывать ошибки. В модуле это можно сделать с помощью try/catch:

try {
    const text = await main();
    console.log(text);
} catch (error) {
    console.error('Ошибка:', error);
}

6. Другие методы

Если вам нужно работать с then и catch, вы также можете это сделать:

main()
    .then(text => {
        console.log('снаружи: ' + text);
    })
    .catch(error => {
        console.error('Ошибка:', error);
    });

7. Совместимость

Top-level await доступен в Node.js начиная с версии 14.8 и в современных браузерах с поддержкой модулей ES, но не работает в скриптах без объявления типа "module".

Заключение

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

Оцените материал
Добавить комментарий

Капча загружается...