JavaScript的异步函数实际上是同步的吗?

8
我正在尝试弄清楚JavaScript中异步代码的工作原理。现在,我了解到JS实际上只有一个线程,在队列中执行作业,并且仅当当前作业完成(即所有同步代码或异步函数已经完成)时才能开始执行下一个作业。
现在,让人困惑的是,什么实际上算作异步函数-什么被放入队列中的单独作业中,什么没有。
首先,我们有函数的“async”关键字。那么这是否意味着这些函数将被放入队列中的单独作业并在将来某个时间执行?嗯,实际上答案是不。但请与我耐心解释。
据我所知,在理论上,JS线程应该从执行所有同步代码开始,直到完成,同时通过将异步函数、Promise和回调放置在队列末尾作为作业来延迟它们的执行。然后,一旦所有同步代码完成,它将开始执行所有这些堆积起来的作业。
因此,如果我有以下代码:
async function asyncFunc() {
    console.log("executing async function");
}

console.log("starting sync code");
asyncFunc().then(() => {
    console.log("executing callback of async function")
});
console.log("sync code completed");

理论上,它应该先执行所有同步代码,然后才开始执行异步函数和回调:

starting sync code
sync code completed
executing async function
executing callback of async function

但实际情况却不同!实际上,它会将异步函数与其他同步代码一起同步执行。实际放入作业队列的只有异步函数的回调函数

starting sync code
executing async function
sync code completed
executing callback of async function

那是什么意思呢?async函数实际上是谎言吗?看起来是这样的,因为它们实际上是您可以附加 异步回调的普通同步函数。
现在,我知道async实际上是一个返回Promise的函数的语法糖,例如:
async function asyncFunc() {
    console.log("executing async function");
}

"

是语法糖,它的含义是:

"
function asyncFunc() {
    return new Promise((resolve) => {
        console.log("executing async function");
        resolve();
    });
}

但是我的观点仍然存在。你传递给Promise的所谓异步函数实际上是同步执行的。嗯,从技术上讲,Promise对象并不意味着它将被异步执行,但async关键字却是如此!所以这是完全错误的信息,它让你相信它是异步的,而事实上它证明它不是。


2
你误解了 async/await 语法的意图。 - Pointy
@MauriceNino 不,JS 中没有任何并行运行的内容,因为只有一个线程。我实际上尝试在异步函数调用后的同步代码中添加了一个 5 秒钟的“忙等待”循环。它会立即执行异步函数,只有在 5 秒钟后才执行回调函数。 - Yevgeni N
1
实际上,最好的理解async的方式是将其视为一种“类型”声明,告诉JavaScript“此函数返回一个或多个Promise对象”。然后,解析器可以将该函数的代码构建为生成器函数,每个内部的await表达式实际上意味着另一个Promise的yield - Pointy
@YevgeniN 虽然它们不是并行运行的,但 JS 并不只有一个线程。Worker 是多线程的一个典型例子。“JavaScript 是单线程的”这种说法过于简化了事实,即用户代码(浏览器内部之外)在主 UI 线程上运行。 - VLAZ
@YevgeniN 我已经发布了我所说的内容的片段。 - MauriceNino
显示剩余3条评论
2个回答

10

就像构造Promise时一样,async函数内任何在遇到await之前的同步操作都会同步执行。直到遇到awaitasync函数才停止执行代码 - 在此之前,它可能与普通非async函数一样(除了它会用一个Promise包装返回值)。

async function asyncFunc2() {
  console.log("in Async function 2");
}
async function asyncFunc1() {
  console.log("in Async function 1");
  await asyncFunc2();
  console.log('After an await');
}
console.log("starting sync code");
asyncFunc1().then(() => {
  console.log("Received answer from async code");
});
console.log("finishing sync code");

如上面的代码片段所示,在 asyncFunc1 中的 await (以及由该 await 调用的所有同步代码) 完成后,主线程才会恢复。

async 是一个关键字,允许您在函数内使用await,但它本身并没有任何其他含义 - 它只是一个关键字。该函数甚至可以完全同步运行其所有代码(尽管这看起来有点奇怪)。


实际上,这似乎也是错误的。它同步执行await后面的代码。例如:async function asyncFunc2() { console.log("in Async func 2"); } async function asyncFunc1() { console.log("in Async func 1"); await asyncFunc2(); } console.log("starting sync code"); asyncFunc1().then(() => { console.log("Received answer from async code"); }); console.log("finishing sync code");输出:starting sync code in Async func 1 in Async func 2 finishing sync code Received answer from async code - Yevgeni N
在这里你不需要等到await之后才记录,因此你会在最后看到“finishing sync code”,就在Promise解析并且“Received answer from async code”之前——你的函数可以是一个return Promise.resolve()。只有在await之后完成某些操作时,你才会看到这些操作被放在较低的同步代码完成之后。 - CertainPerformance
3
是的,这使它们不是真正的“异步”函数,而是“仅在存在await关键字的行之后才是异步”的函数。 - Yevgeni N
1
@YevgeniN 我认为你应该更深入地了解async/await的实际含义。第一个awaitreturn new Promise ...部分所在的位置。 - MauriceNino
1
@YevgeniN 是的,这是一个非常精确的表述。您可以放置一个 Promise.resolve().then(asyncFunc1) 来在所有同步低代码完成后运行一些东西。 - CertainPerformance

1

为了证明我和@CertainPerformance的观点。 这是一个带有等待500ms才解析并等待的睡眠函数的相同示例:

async function sleep(msec) {
  return new Promise(resolve => setTimeout(resolve, msec));
}

async function asyncFunc() {
  await sleep(500);
  console.log("executing async function");
}

console.log("starting sync code");
asyncFunc().then(() => {
  console.log("executing callback of async function")
});
console.log("sync code completed");

这里只需要一个console.log:

async function sleep(msec) {
  console.log('test');
} 

async function asyncFunc() {
  await sleep(500);
  console.log("executing async function");
}

console.log("starting sync code");
asyncFunc().then(() => {
  console.log("executing callback of async function")
});
console.log("sync code completed");


是的,这个方法可行,因为setTimeout总是会安排一个新的任务。这似乎是唯一强制代码异步执行的方式。但是,如果你用console.log替换setTimeout(resolve, msec),它仍然会同步调用。 - Yevgeni N
我是说 console.log 本身会同步调用。我现在明白了,只有在带有 await 关键字的那一行后面的代码才会异步调用。 - Yevgeni N

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