JavaScript中没有await的异步函数

132

我有两个异步函数,ab,前者没有使用await,后者使用了await。它们都会向控制台输出一些内容,并返回undefined。在调用这两个函数之后,我会再次记录一条消息,并查看该消息是在函数体执行之前还是之后被写入。

function someMath() {
  for (let i = 0; i < 9000000; i++) { Math.sqrt(i**5) }
}

function timeout(n) {
   return new Promise(cb => setTimeout(cb, n))
}

// ------------------------------------------------- a (no await)
async function a() {
  someMath()
  console.log('in a (no await)')
}

// ---------------------------------------------------- b (await)
async function b() {
  await timeout(100)
  console.log('in b (await)')
}

clear.onclick = console.clear

aButton.onclick = function() {
  console.log('clicked on a button')
  a()
  console.log('after a (no await) call')
}

bButton.onclick = function() {
  console.log('clicked on b button')
  b()
  console.log('after b (await) call')
}
<button id="aButton">test without await (a)</button>
<button id="bButton">test with await (b)</button>
<button id="clear">clear console</button>

如果您在没有使用await的情况下启动测试,该函数似乎会像同步函数一样工作。但是,使用await时,由于该函数是异步执行的,消息会被倒置

当没有await关键字时,JavaScript如何执行async函数?


真实用例:我有一个有条件执行的await关键字,我需要知道该函数是同步执行还是异步执行,以便渲染我的元素:

async function initializeComponent(stuff) {
   if (stuff === undefined)
      stuff = await getStuff()
   // Initialize

   if (/* Context has been blocked */)
       renderComponent() // Render again if stuff had to be loaded
}

initializeComponent()
renderComponent()

P.S: 为避免与其他语言中的相似问题混淆(例如在不使用await的情况下使用async),本标题含有JavaScript关键字。


您还需要将 bButton.onclick 函数转换为异步函数,并等待 b() 结束,以便获得所需的日志。 - Jose Hermosilla Rodrigo
@JoseHermosillaRodrigo 我不想等待所需的日志,我想知道使用或不使用 await 关键字是否会改变同步函数。如果不是这样,也许是我的测试有问题。最后,我会更新我的真实用例,也许对您来说会更清晰。 - Ulysse BN
1
推荐阅读 - https://medium.com/javascript-in-plain-english/async-await-javascript-5038668ec6eb - arcseldon
2
"await" 关键字是导致异步的原因。如果没有 "await",则语句将同步执行。 但是,如果您想让函数返回一个值,则 "async" 会有所不同。没有 "async",您只能得到一个值;但是使用 "async",您会得到一个 promise,并且需要等待该值,或者在 .then() 回调中获取它,这是异步的。即使没有 "await",'async' 对函数的调用者也可能产生影响。 - Max Waterman
4个回答

115

Mozilla文档:

异步函数可以包含一个等待表达式(await expression),它会暂停异步函数的执行并等待传递过来的Promise对象被解析,然后恢复异步函数的执行并返回所解析的值。

如果没有等待表达式,则代码将按照正常的同步方式执行。


11
有没有针对多余的async关键字的eslint规则?我认为同步函数不应该有它。 - phil294
36
在返回一个Promise的函数中声明“async”,但不使用await的一个好处是异常会被拒绝(rejected),而不是同步抛出。例如:const result = someAsyncFunctionWithoutAwait().catch(handleRejection) - aaaaaa
8
换句话说,这个答案是否应该说:“如果没有 await 存在,执行不会暂停,但您的代码仍将以非阻塞方式执行”,而不是 “如果没有 await 存在,执行不会暂停,并且你的代码将同步执行”?此外,如果没有 async 和 await 存在,您的代码仍将是非阻塞的(例如,使用 async 关键字声明函数但不使用 await 等效于不使用 async 关键字声明该函数)。 - user3773048
2
一个很好的理解方式是,Async/Await允许您同步异步代码:async function dummy(){ /*...*/ return Promise.resolve(/*...*/); } async dummyAsync(){ dummy(); nextDummy(); } async dummySync(){ await dummy(); nextDummy(); }。在dummyAsync中,没有任何东西可以阻止nextDummy()dummy()完成之前执行;它是异步的。在dummySync中,只有在来自dummy()的promise解析(返回)后,nextDummy()才会执行;它是同步的,或者说是过程化编写的异步代码。@AdrianPop提供了一个完美的使用声明异步而不调用等待的示例。 - Rik
1
@user3773048 正常的代码以阻塞方式执行,而不是“非阻塞”执行。 - Bergi
显示剩余10条评论

74

在执行 JavaScript 异步函数之前,一切都是同步的。在使用 async-await 时,await 是异步的,而等待后的所有内容都会被放置在事件队列中,类似于 .then()

为了更好地解释,以此示例为例:

function main() {
  return new Promise( resolve => {
    console.log(3);
    resolve(4);
    console.log(5);
  });
}

async function f(){
    console.log(2);
    let r = await main();
    console.log(r);
}

console.log(1);
f();
console.log(6);

由于await是异步的,而其他部分包括promise都是同步的,因此输出结果为

1
2
3
5
6
// Async happened, await for main()
4

没有使用Promise的main()也有类似的行为:

function main() {
    console.log(3);
    return 4;
}

async function f(){
    console.log(2);
    let r = await main();
    console.log(r);
}

console.log(1);
f();
console.log(5);

输出:

1
2
3
5
// Asynchronous happened, await for main()
4

只需删除 await 关键字,整个 async 函数就会成为同步函数。

function main() {
    console.log(3);
    return 4;
}

async function f(){
    console.log(2);
    let r = main();
    console.log(r);
}

console.log(1);
f();
console.log(5);

输出:

1
2
3
4
5

2
你可以添加一个函数,执行 var g = await f(),因为 f() 是异步的,如果你不想阻塞主执行线程,可能需要在长时间运行的过程中等待它。 - flux9998
1
"await" 只能在 "async" 函数中使用。 - NAVIN
3
请注意,这是 ES11 之前的旧实现。在 ES11 中有一些变化,但这仍然可以工作。 - NAVIN
4
在事件队列中的代码(await之后的所有代码)是否总是在主线程完成后运行?我的意思是,在您的两个示例中,最后一个 console.log 之后,事件队列运行,并且正在等待的代码最终执行。如果我们有更多指令和更多代码,那么在最后一条指令之后,等待的代码是否仍然会运行? - chris
最后一个例子应该在main()中有一个Promise,这样它就不会是同步的。因此删除await并不一定会使执行成为同步。 - user2734550

40

使用或不使用 await 执行函数的效果相同。 await 的作用是自动等待函数返回的 Promise 被解决。

await timeout(1000);
more code here;

大致相当于:

timeout(1000).then(function() {
    more code here;
});

async function 声明只是让函数自动返回一个 promise,在函数返回时解决该 promise。


1
有用的答案 - 如果需要更详细的解释,也可以尝试这篇博客 - https://medium.com/javascript-in-plain-english/async-await-javascript-5038668ec6eb - arcseldon

19

就像其他答案所说/表明的那样:一个 async function 在遇到 await 之前会一直执行 - 如果没有 await,它将完全运行。

值得补充的是,async 总是将您的结果变成一个 Promise。因此,如果您返回某些内容,已经存在差异,您不能简单地在未先将其返回到 JS 引擎中就获取结果(类似于事件处理):

async function four(){
  console.log("  I am four");
  return 4;
}
console.log(1);
let result=four();
console.log(2,"It is not four:",result,"Is it a promise ?", result instanceof Promise);
result.then(function(x){console.log(x,"(from then)");});
console.log(3);


@UlysseBN 我想我找到了折中的方法 :-). 虽然这里出现的 {} 不是很引人注目,但它已经表明它肯定不是 4,而且值得一提的是,在浏览器的 JS 控制台中,可以看到并查看实际对象。 - tevemadar
是的,我猜到这就是你期望的!不幸的是,我不知道在JS中展示一个实例来自哪个类的好方法...(但可以使用instanceof)。 - Ulysse BN
因此,虽然async函数'four()'内部的代码是同步运行的,但为了获取返回值(4),调用者需要使用'await'或'.then()'进行异步操作。 在某些情况下,这是非常不同的,对吧? - Max Waterman
@MaxWaterman 当然可以。请记住,这里每个答案的上下文都是原始问题:如果有人编写了一个async函数,但没有在内部使用await,会发生什么。这在实际代码中并不是非常有用,因此几乎任何实际代码都会有所不同。 - tevemadar
我不明白为什么“4(从那时起)”行在3之后被打印出来。这是为什么呢? - Aeternus
因为Promise的完成实际上是一个事件,而then(...)就是它的事件处理程序。JavaScript引擎只有在当前运行的代码返回到它之后才会处理事件(这是网页冻结的经典原因之一:编写长时间运行的事件处理程序,以便其他事件无法被处理)。 - tevemadar

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