当异步函数不返回 Promise 时,为什么我需要等待它?

IT技术 javascript node.js ecmascript-6 promise async-await
2021-02-28 20:52:13

考虑这个代码:

async function load() {
  const data = await new Promise(resolve => {
    setTimeout(() => resolve([1, 2, 3]), 10);
  }).then(data => data.map(i => i * 10));
  console.log(`Data inside the function: ${JSON.stringify(data)}`);
  return data;
}

function main() {
  const data = load();
  console.log(`Loaded data: ${JSON.stringify(data)}`);
}

main();

这是我得到的输出:

Loaded data: {}
Data inside the function: [10,20,30]

但是如果我把代码改成这样:

异步函数加载(){
  const 数据 = 等待新的Promise(解决 => {
    setTimeout(() => resolve([1, 2, 3]), 10);
  }).then(data => data.map(i => i * 10));
  console.log(`函数内的数据:${JSON.stringify(data)}`);
  返回数据;
}

异步函数 main() {
  const 数据 =等待加载();
  console.log(`加载的数据:${JSON.stringify(data)}`);
}

主要的();

我会得到这个:

Data inside the function: [10,20,30]
Loaded data: [10,20,30]

我很困惑,因为根据文档await应该暂停执行,直到Promise得到解决。在这种情况下,第一个示例应data作为数组返回但正如你所看到的,它返回 aPromise而我不知道为什么!?

同时,文档中有这部分我不明白它在说什么:

await 可以拆分执行流,允许 await 函数的调用者在 await 函数的延迟继续之前恢复执行。在 await 推迟其函数的继续执行之后,如果这是该函数执行的第一个 await,则立即执行也会通过向函数的调用者返回一个挂起的 Promise 以完成 await 的函数并恢复该调用者的执行。

在我看来,await只有当您的代码中的所有函数都是async可笑的时才有效,因为如果我使用另一个module中的函数,我怎么知道它是否是一个async!?或者,也许我应该采取谨慎的态度,始终使用 an 调用所有函数await,无论它们是否async存在!!!

[更新]

感谢所有参与并为我提供洞察力的人。但我仍然很困惑我应该如何使用awaitand async我应该总是用 调用我的所有函数await吗?

假设我正在编写由多个文件中的多个函数组成的代码。如果我最终使用一个返回 aPromise或它是一个async函数的库,我是否应该将所有函数调用从异步点追溯到应用程序的入口点,并await在所有函数调用之前添加一个async或者,也许我应该养成使用 an 调用所有函数的习惯,await而不管它们是否async存在?

3个回答

所有async函数都返回一个Promise。他们都是。

该Promise最终将使用您从异步函数返回的任何值来解决。

await只阻止async函数内部的执行它不会阻塞函数之外的任何东西。从概念上讲,异步函数开始执行,一旦遇到await指令,它会立即从函数返回一个未实现的Promise,外部执行世界得到该Promise并继续执行。

稍后,被awaited的内部 promise 将被解析,然后函数内部其余部分的执行将继续。最终函数的内部将完成并返回一个值。这将触发解析从具有该返回值的函数返回的Promise。

仅供参考,您的load()函数中有很多多余的东西你可以从这个改变它:

async function load() {
  const data = await new Promise(resolve => {
    setTimeout(() => resolve([1, 2, 3]), 10);
  }).then(data => data.map(i => i * 10));
  console.log(`Data inside the function: ${JSON.stringify(data)}`);
  return data;
}

对此:

function load() {
    return new Promise(resolve => {
        setTimeout(() => resolve([1, 2, 3]), 10);
    }).then(data => data.map(i => i * 10));
}

然后,像这样使用它:

load().then(result => {
    console.log(result);
});

或者,我更喜欢将手动创建 promise 封装在自己的函数中,如下所示:

function delay(t, v) {
    return new Promise(resolve => {
        setTimeout(resolve.bind(null, v), t);
    });
}

function load() {
    return delay(10, [1, 2, 3]).then(data => data.map(i => i * 10));
}

而且,事实证明,这个小delay()函数通常在您想要延迟Promise链的许多地方很有用。


感谢所有参与并为我提供洞察力的人。但我仍然很困惑我应该如何使用 await 和 async。

首先,大多数情况下,只有async在需要await在函数内部使用时才标记函数。

其次,当您有多个异步操作并且想要对它们进行排序时,最常使用await(从async函数内) - 通常是因为第一个操作提供的结果用作第二个操作的输入。await当您只有一个异步操作时,您可以使用它,但与简单的.then().

以下是一些使用 的充分理由的示例async/await

对多个异步操作进行排序

想象一下,你有getFromDatabase()getTheUrl()而且getTheContent()都是异步的。如果有任何失败,您只想拒绝返回的第一个错误的Promise。

这是没有 async/await 时的样子:

function run() {
    return getFromDatabase(someArg).then(key => {
        return getTheURL(key);
    }).then(url => {
        return getTheContent(url);
    }).then(content => {
         // some final processing
         return finalValue;
    });
}

这是它的外观async/await

async function run(someArg) {
    let key = await getFromDatabase(someArg);
    let url = await getTheURL(key);
    let content = await getTheContent(url);
    // some final processing
    return finalValue;        
}

在这两种情况下,该函数都会返回一个使用 finalValue 解析的Promise,因此调用者使用这两个实现相同:

run(someArg).then(finalValue => {
    console.log(finalValue);
}).catch(err => {
    console.log(err);
});

但是,您会注意到该async/await实现具有更多的序列化、同步外观,并且看起来更像是非异步代码。许多人发现这更易于编写、阅读和维护。步骤之间的处理越多,包括分支,async/await版本的优势就越大

自动捕获被拒绝的Promise和同步异常

正如我之前所说,async函数总是返回一个Promise。他们还必须内置错误处理,自动将错误传播回返回的Promise。

不用说,如果您手动从async函数返回一个Promise并且该Promise被拒绝,那么从该async函数返回的Promise将被拒绝。

而且,如果您正在使用await并且正在等待的任何Promise被拒绝,并且您.catch()在Promise上没有 a 并且try/catch周围没有 a ,那么函数返回的Promise将自动拒绝。所以,回到我们之前的例子中:

async function run(someArg) {
    let key = await getFromDatabase(someArg);
    let url = await getTheURL(key);
    let content = await getTheContent(url);
    // some final processing
    return finalValue;        
}

如果三个Promise中的任何一个被await拒绝,那么该函数将短路(停止在函数中执行更多代码)并拒绝异步返回的Promise。因此,您可以免费获得这种形式的错误处理。

最后,一个async函数还会为您捕获同步异常并将它们转换为被拒绝的Promise。

在一个像我们之前那样返回Promise的普通函数中:

function run() {
    return getFromDatabase(someArg).then(key => {
        return getTheURL(key);
    }).then(url => {
        return getTheContent(url);
    }).then(content => {
         // some final processing
         return finalValue;
    });
}

如果getFromDatabase()抛出同步异常(可能是因为someArg无效而触发),那么这个整体函数run()会同步抛出。这意味着调用者要从 中捕获所有可能的错误run(),他们都必须用 a 包围它try/catch以捕获同步异常并使用 a.catch()来捕获被拒绝的Promise:

try {
    run(someArg).then(finalValue => {
        console.log(finalValue);
    }).catch(err => {
        console.log(err);
    });
} catch(e) {
    console.log(err);
}

这是凌乱的,有点重复。但是,当run()被声明时async,它将永远不会同步抛出,因为任何同步异常都会自动转换为被拒绝的Promise,因此您可以确保在以这种方式编写时捕获所有可能的错误:

async function run(someArg) {
    let key = await getFromDatabase(someArg);
    let url = await getTheURL(key);
    let content = await getTheContent(url);
    // some final processing
    return finalValue;        
}

// will catch all possible errors from run()
run(someArg).then(finalValue => {
    console.log(finalValue);
}).catch(err => {
    console.log(err);
});

我应该总是用 await 调用我的所有函数吗?

首先,您只会使用await返回Promise的函数,因为await如果该函数不返回Promise(如果不需要,只会增加代码的混乱)则没有用处。

其次,您是否使用await取决于调用函数的上下文(因为您必须在async要使用函数中await以及逻辑流以及它是否从使用中受益await

使用 await 毫无意义的地方

async function getKey(someArg) {
    let key = await getFromDatabase(someArg);
    return key;
}

await这里没有做什么有用的东西。您没有对多个异步操作进行排序,也没有对返回值进行任何处理。您可以通过直接返回Promise来完成完全相同的代码:

async function getKey(someArg) {
    return getFromDatabase(someArg);
}

而且,如果您知道getFromDatabase()永远不会同步抛出,您甚至可以async从声明中删除

function getKey(someArg) {
    return getFromDatabase(someArg);
}

假设我正在编写由多个文件中的多个函数组成的代码。如果我最终使用一个返回 Promise 的库或者它是一个异步函数,我是否应该将所有函数调用从异步点追溯到应用程序的入口点,并在使它们异步之后在所有函数调用之前添加一个等待?

这个问题有点过于笼统,在一般情况下很难回答。以下是沿着这个大方向的一些想法:

  1. 一旦您尝试从函数返回的结果的任何部分A()是异步的或使用任何异步操作来获取,该函数本身就是异步的。在纯 Javascript 中,您永远无法同步返回异步结果,因此您的函数必须使用异步方法来返回结果(Promise、回调、事件等)。

  2. 任何B()调用异步函数的函数A()也试图根据其获取的结果返回结果,A()现在也是异步的,并且还必须使用异步机制将其结果传回。对于C()调用B()并需要将其结果传达给调用者的函数来说这是正确的。因此,您可以说异步行为具有传染性。直到您到达调用链中不再需要返回结果的某个点之前,一切都必须使用异步机制来传达结果、错误和完成。

  3. 没有特别需要标记一个函数,async除非您特别需要一个async函数的好处之一,例如await在该函数内部使用的能力或它提供的自动错误处理。您可以编写返回 Promise 的函数,而无需async在函数声明中使用。所以,“不”我不会回到调用链上做所有事情async如果有特定的原因,我只会使函数异步。通常那个原因是我想使用await在函数内部,但也有同步异常的自动捕获,这些异常会变成我之前描述的Promise拒绝。对于行为良好的代码,您通常不需要它,但有时对于行为不佳的代码或具有未定义行为的代码很有用。

  4. await也仅在有特定原因时使用。我不只是在每个返回Promise的函数上自动使用它。我已经在上面描述了使用它的原因。仍然可以使用.then()justfine 来处理返回Promise的单个函数调用的结果。在某些情况下,是否要使用.then()只是个人风格的问题,await并且没有特别的理由必须以一种或另一种方式使用。

或者也许我应该养成使用 await 调用所有函数的习惯,而不管它们是否异步?

绝对不!首先,您要做的最后一件事是采用完全同步的代码并不必要地使其异步,甚至使其看起来是异步的。异步代码(即使使用asyncawait)比同步代码更难以编写、调试、理解和维护,因此您永远不会希望通过添加async/await同步代码中而不必要地将同步代码变成异步代码

例如,你永远不会这样做:

async function random(min, max) {
    let r = await Math.random();
    return Math.floor((r * (max - min)) + min);
}

首先,这是一个完全同步的操作,可以这样编码:

function random(min, max) {
    let r = Math.random();
    return Math.floor((r * (max - min)) + min);
}

其次,第一个async实现使该函数很难使用,因为它现在具有异步结果:

 random(1,10).then(r => {
     console.log(r);
 });

而不仅仅是简单的同步使用:

 console.log(random(1,10));
@Paulpro - 好吧,这似乎使用外部代码显然阻塞了整个 Javascript 线程,这也可以使用syncchild_process 中api来完成,但这些都不是我们在这里真正谈论的内容。
2021-04-23 20:52:13
@Mehran - 你不能在 Javascript 中做到这一点。您永远无法获取异步获取的值并同步返回它。异步/等待不会这样做。Javascript 中没有这样做。你总是从一个async函数中得到一个返回的 Promise,并且await在该函数中没有使用 来改变它。请阅读我的答案的详细信息,以更好地了解该async功能的工作原理。
2021-05-03 20:52:13
@Paulpro 但是你不能在 javascript 中做到这一点仍然是真的 - 你需要在 C 中做到(你可以在 node.js 中做到这一点,但使用节点的 C API 而不是节点的 javascript API)
2021-05-04 20:52:13
@jfriend00 您可以同步返回一个异步值。(例如 npm 中的同步 rpc)。根本没有理由这样做。
2021-05-14 20:52:13
我的load()功能的重点是做一些异步操作并使用await
2021-05-17 20:52:13

async/await只是语法糖,这意味着,它们不会为语言带来任何新功能,只是对 Promise 有用的包装器。

如果一个函数被标记为async,它总是返回一个Promise:

> async function f() { return 42; }
undefined
> f()
Promise { 42 }

此外,如果一个函数是async,您可以await在其中处理任何Promise(包括另一个async函数的结果),并且函数代码的执行将暂停,await直到该Promise被解决或拒绝。

回答你的问题:如果你使用库函数,你通常知道它是否返回一个Promise(如果它被标记为async,它肯定会)。因此,请确保await为它或.then与返回的Promise一起使用。

我并不是说粗鲁,但是您多久阅读一次库的代码以查看您正在使用的功能是否正确async
2021-04-28 20:52:13
我不同意 async/await 不会为语言带来新功能。它们允许您以比以前更简单的方式编写异步代码,尤其是在对多个异步操作进行排序时。虽然你可以在没有它们的情况下编写任何东西,但你也可以在没有Promise的情况下编写任何东西,但Promise也为语言带来了许多新功能。
2021-04-28 20:52:13
@Mehran - 好吧,这只是你的意见。它们是非常有用的工具,可以使异步编码的编码、阅读和维护变得更加简单。
2021-04-29 20:52:13
我完全同意这一点awaitasync并使您的代码更具可读性。我想说的是,他们不是他们应该成为的人。在我看来,它们的实施是错误的。
2021-05-14 20:52:13
@Mehran - 您需要做的就是查看函数的接口,看看它是否返回Promise。您实际上并不关心是否比 promise 自动返回,因为它被标记为一个async函数,或者 promise 是否是手动返回的。无论哪种方式,接口都是相同的 - 调用者处理返回的Promise。
2021-05-19 20:52:13

因为第一个函数是异步的——所以它在main函数的其余部分执行时运行,这在下一行记录结果时没有帮助。在使用结果之前,您必须等待函数完成执行 - 所以在您的示例中使用async/ await

async function main() {
  const data = await load();
  console.log(`Loaded data: ${JSON.stringify(data)}`);
}

或使用.then

function main() {
  load().then(data => {
    console.log(`Loaded data: ${JSON.stringify(data)}`);
  });
}

这里的提示是:如果函数async,你必须及时使用async,因为它总是返回一个 Promise。