如何在顶层使用async/await?

487

我一直在学习async/await,看了好几篇文章后,打算亲自测试一下。但是,我似乎无法理解为什么以下代码不起作用:

async function main() {  
    var value = await Promise.resolve('Hey there');
    console.log('inside: ' + value);
    return value;
}

var text = main();  
console.log('outside: ' + text);

控制台输出如下内容(使用 node v8.6.0):

> 外部:[object Promise]

> 内部:嘿,你好

为什么函数内的日志消息会在之后执行?我认为创建 async/await 的原因是为了使用异步任务执行同步操作。

在不使用 .then() 后跟 main() 的情况下,有没有一种方法可以使用函数内返回的值?


21
不,只有时间机器才能使异步代码变成同步。await 只是 Promise then 语法的简化形式。 - Bergi
2
@estus 这只是我在 Node 中测试时使用的一个快速函数名称,不一定代表程序的 main - Felipe
2
FYI,“async/await”是ES2017的一部分,而不是ES7(ES2016)。 - Felix Kling
1
对于交互式节点 shell (REPL),请尝试使用 node --experimental-repl-await - nomad
1
用发髻,对我来说完美无缺。 - undefined
显示剩余5条评论
15个回答

566

我似乎无法理解为什么这不起作用。

因为 main 返回一个承诺;所有的 async 函数都是这样。

在顶层,您必须要么:

  1. 使用顶级 await提案MDN;ES2022,现代环境下广泛支持)允许在模块中的顶级使用 await

    或者

  2. 使用一个永远不会拒绝的顶级 async 函数(除非您想要“未处理的拒绝”错误)。

    或者

  3. 使用 thencatch

#1 在模块中使用顶级 await

您可以在模块的顶层使用 await。直到您await的承诺解决(意味着任何等待您的模块加载的模块在承诺解决之前都无法完成加载),您的模块才会完成加载。如果该承诺被拒绝,您的模块将无法加载。通常,在您的模块不能执行其工作直到承诺解决且除非承诺被履行否则完全无法执行其工作的情况下,会使用顶级 await,这是可以接受的:

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

如果你的模块即使 Promise 被拒绝也能继续工作,那么你可以在顶层的 await 前加上一个 try/catch

// In a module, once the top-level `await` proposal lands
try {
    const text = await main();
    console.log(text);
} catch (e) {
    // Deal with the fact the chain failed
}
// `text` is not available here
当使用顶层await的模块被评估时,它会向模块加载器返回一个Promise(就像async函数一样),在依赖它的任何模块的主体评估之前等待该Promise完成。
您不能在非模块脚本的顶层使用await,只能在模块中使用。
#2 - 顶级async函数永不拒绝
(async () => {
    try {
        const text = await main();
        console.log(text);
    } catch (e) {
        // Deal with the fact the chain failed
    }
    // `text` is not available here
})();
// `text` is not available here, either, and code here is reached before the promise settles
// and before the code after `await` in the main function above runs

注意catch语句; 你必须处理Promise的拒绝/异步异常,因为没有其他东西会这样做; 你没有调用者可以将它们传递下去(与#1不同,在那里你的“调用者”是模块加载器)。如果你愿意,你可以在通过catch函数调用它的结果上执行(而不是try / catch语法):

(async () => {
    const text = await main();
    console.log(text);
})().catch(e => {
    // Deal with the fact the chain failed
});
// `text` is not available here, and code here is reached before the promise settles
// and before the code after `await` in the main function above runs

这样写会更简洁一些,虽然它将 (async/await 和显式 promise 回调) 进行了混合,通常我不建议这样做。

当然,你也可以选择不处理错误,只允许出现“未处理的拒绝”错误。

#3 - thencatch

main()
    .then(text => {
        console.log(text);
    })
    .catch(err => {
        // Deal with the fact the chain failed
    });
// `text` is not available here, and code here is reached before the promise settles
// and the handlers above run

catch处理程序将在链中或您的then处理程序中发生错误时调用。 (确保您的catch处理程序不会引发错误,因为没有注册任何内容来处理它们。)

或者是then的两个参数:

main().then(
    text => {
        console.log(text);
    },
    err => {
        // Deal with the fact the chain failed
    }
);
// `text` is not available here, and code here is reached before the promise settles
// and the handlers above run

再次注意我们正在注册一个拒绝处理程序。但是在这种形式下,请确保您的then回调都不会抛出任何错误,因为没有任何内容被注册来处理它们。


把它看作一个承诺,就能解释为什么函数立即返回了。我尝试创建一个顶层匿名异步函数,现在得到了有意义的结果。 - Felipe
4
@Felipe: 是的,async/await是围绕Promise的语法糖(好的那种语法糖 :-))。你不仅仅认为它返回了一个Promise;它实际上就是返回一个Promise。(详情 - T.J. Crowder
1
@LukeMcGregor - 我展示了上面两种方式,首先是使用 all-async 选项。对于顶层函数,我可以看到两种方式(主要是因为 async 版本有两个缩进级别)。 - T.J. Crowder
3
@Felipe - 我已经更新了答案,现在顶层await提议已经达到了第三阶段。 :-) - T.J. Crowder
顶级等待现在已经达到第四阶段。由于这是全新的语法,不确定支持有多广泛。主要考虑那些存在一段时间的旧浏览器版本。Webpack 5 和 Babel 都知道这种语法。 - wheredidthatnamecomefrom
显示剩余3条评论

55
2023年的答案:您现在可以在所有支持的Node.js版本中使用顶级等待(top level await)。
上面的大多数答案都有点过时或者非常冗长,所以这里给出一个适用于Node 14及以上版本的快速示例。
在JavaScript中(不使用TypeScript):
下面有一个单独的TypeScript答案。
创建一个名为runme.mjs的文件:
import * as util from "util";
import { promisify } from "util";
const exec = promisify(lameExec);
const log = console.log.bind(console);

// Top level await works now
const { stdout, stderr } = await exec("ls -la");
log("Output:\n", stdout);
log("\n\nErrors:\n", stderr);


运行 node runme.mjs
Output:
 total 20
drwxr-xr-x  2 mike mike 4096 Aug 12 12:05 .
drwxr-xr-x 30 mike mike 4096 Aug 12 11:05 ..
-rw-r--r--  1 mike mike  130 Aug 12 12:01 file.json
-rw-r--r--  1 mike mike  770 Aug 12 12:12 runme.mjs



Errors:

你不需要在你的package.json中设置type: module,因为.mjs文件默认被认为是ESM。

在TypeScript中使用esrun

在TypeScript中,使用esrun。
esrun比ts-node和tsx更好,因为:
- esrun不需要更改package.json - esrun不需要tsconfig.json - esrun从不说它不知道如何运行ts文件 - esrun默认使用当前版本的JavaScript
npm i @digitak/esrun

运行只需运行TS与npx esrun file.ts
Output:
 total 128
drwxr-xr-x@  8 mikemaccana  staff    256 18 Jul 13:50 .
drwxr-xr-x@ 13 mikemaccana  staff    416 17 Jul 14:05 ..
-rw-r--r--@  1 mikemaccana  staff     40 18 Jul 13:27 deleteme.ts
-rw-r--r--@  1 mikemaccana  staff    293 18 Jul 13:51 deletemetoo.ts
drwxr-xr-x@ 85 mikemaccana  staff   2720 18 Jul 13:51 node_modules
-rw-r--r--@  1 mikemaccana  staff  46952 18 Jul 13:51 package-lock.json
-rw-r--r--@  1 mikemaccana  staff    503 18 Jul 13:51 package.json
-rw-r--r--@  1 mikemaccana  staff   1788 18 Jul 13:48 utils.ts

⚠️ 你不需要在你的 package.json 中设置 type: module,因为 esrun 有智能默认值。

2
我正在使用Next.js,这对我很有帮助:https://dev59.com/zFEG5IYBdhLWcg3wTZp7#68339259 - Ryan
3
请确保在您的 package.json 文件中使用 "type": "module" - Ryan Norooz
1
@RyanNorooz 如果在 package.json 中没有 "type": "module",它将是有价值的。 - mikemaccana
@Bersan,那是不正确的,你在package.json中不需要type: module。所有的.mjs文件都被默认为ESM。请参考你的链接:https://www.typescriptlang.org/docs/handbook/esm-node.html#new-file-extensions 试一下吧。 - mikemaccana
1
@mikemaccana 我明白了。使用.mts扩展名时,它会一直假设为 ESModule(对于 TypeScript 的情况)。 - Bersan
显示剩余6条评论

35

顶层 await 已经移动到第四阶段(请参见namo的评论),因此回答你的问题如何在顶层使用async/await?,只需要使用await即可:

const text = await Promise.resolve('Hey there');
console.log('outside: ' + text)

如果你想要一个main()函数:在对main()的调用中添加await

async function main() {
    var value = await Promise.resolve('Hey there');
    console.log('inside: ' + value);
    return value;
}

var text = await main();  
console.log('outside: ' + text)

兼容性


1
--harmony-top-level-await 对我没有起作用,我使用的是 Node 14。 - Quinten C
7
可能是因为您没有使用 ES 模块。为了确保 Node 处于模块模式下,请在 package.json 中添加 "type": "module" - Almeo Maus
1
2021年5月25日,状态更新为第四阶段。 - Namo

15

在当前答案的基础上提供一些更多的信息:

node.js 文件的内容目前以字符串方式拼接在一起,形成一个函数体。

例如,如果您有一个文件test.js

// Amazing test file!
console.log('Test!');

那么 node.js 将会暗自串联一个看起来像这样的函数:

function(require, __dirname, ... perhaps more top-level properties) {
  // Amazing test file!
  console.log('Test!');
}

需要注意的主要事项是,得到的函数不是异步函数。因此,您不能在其中直接使用await关键字!

但是假设您需要在这个文件中使用promises,那么有两种可能的方法:

  1. 不要在函数内部直接使用await
  2. 完全不要使用await

选项1要求我们创建一个新的作用域(并且此作用域可以是async,因为我们对其具有控制权):

// Amazing test file!
// Create a new async function (a new scope) and immediately call it!
(async () => {
  await new Promise(...);
  console.log('Test!');
})();

选项2要求我们使用面向对象的Promise API(使用Promise的不太美观但同样具有功能性的范例)

// Amazing test file!
// Create some sort of promise...
let myPromise = new Promise(...);

// Now use the object-oriented API
myPromise.then(() => console.log('Test!'));

看到Node.js支持顶级await会很有趣!


3
Node在v13.3版本中添加了顶层await的支持,需要打开一个标志才能使用。 - Dan Dascalescu

9

您现在可以在 Node v13.3.0 中使用顶级 await。

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


1
该发布日志没有提到顶级等待的任何内容,而且似乎对该标志的支持始于v13.3 - Dan Dascalescu
使用 Node 版本 18/19,我得到了 node: bad option: --harmony-top-level-await 的错误提示,并且顶层 await 也无法正常工作,非常令人困惑。我原以为这是一个几乎可以保证的新功能。 - Alexander Mills

6
这个问题的实际解决方法是以不同的方式来处理它。
你的目标可能是某种初始化,通常发生在应用程序的顶层。
解决方案是确保应用程序的顶层只有一个JavaScript语句。如果在应用程序的顶部只有一个语句,那么您可以在任何其他地方自由使用async/await(当然,要遵守正常的语法规则)。
换句话说,将整个顶层包装在一个函数中,这样它就不再是顶层,这解决了如何在应用程序的顶层运行async/await的问题-您不需要这样做。
这是应用程序顶级应该看起来的样子:
import {application} from './server'

application();

1
你说得对,我的目标是初始化。例如数据库连接、数据提取等等。在某些情况下,在继续应用程序的其余部分之前,需要获取用户的数据。你的意思是建议将application()设置为异步吗? - Felipe
1
不,我只是在说,如果你的应用程序根目录下只有一个JavaScript语句,那么你的问题就解决了 - 如所示的顶级语句不是异步的。问题在于无法在顶层使用async - 你无法让await在该级别实际等待 - 因此,如果顶层只有一个语句,那么你已经绕过了这个问题。你的初始化异步代码现在在一些导入的代码中,因此异步将正常工作,并且你可以在应用程序启动时初始化所有内容。 - Duke Dougal
1
更正 - 应用程序是异步函数。 - Duke Dougal
6
抱歉,我没有表达清楚。我的意思是,通常情况下,在顶层级别,异步函数不会等待... JavaScript 直接执行下一条语句,因此您无法确定初始化代码是否已完成。 如果应用程序顶部只有一个单独的语句,那么这就没有关系。 - Duke Dougal

5

对于浏览器,您需要添加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="await.js" -->
<script type="module">
  const resp = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await resp.json();
  console.log(users)
</script>


简明扼要的回答!我正在寻找一个在HTML中“包含”的脚本,而不是被导入的随机JS文件。 - codepleb

5

我喜欢这种聪明的语法,在入口点执行异步工作。

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

漂亮的语法!我从未见过像那样使用void。如果有人好奇,这就是为什么它能够工作的原因:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void。只是随便提一下:如果你只是使用单个异步函数或者你不关心多个异步函数执行的顺序,你也可以使用`void doSomeWork()。Void本质上会丢弃任何返回的值,并返回undefined`。 - Themis
对于一个简单的 TypeScript 项目,这真的很有帮助,因为我不必在 tsconfig.json 中进行太多的配置工作。谢谢! - HoangLM

4

其他解决方案缺乏符合POSIX标准的重要细节:

你需要...

  • 成功时报告0退出状态,失败时报告非零值。
  • 将错误输出到 stderr 输出流。
#!/usr/bin/env node

async function main() {
 // ... await stuff ... 
}

// POSIX compliant apps should report an exit status
main()
    .then(() => {
        process.exit(0);
    })
    .catch(err => {
        console.error(err); // Writes to 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) => {
    // run some async action
  });

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

3

Node -
您可以在REPL中运行node --experimental-repl-await。关于脚本部分我不是很确定。

Deno -
Deno已经内置了它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接