当异步函数不应该返回 Promise 时,为啥我需要等待它?

Posted

技术标签:

【中文标题】当异步函数不应该返回 Promise 时,为啥我需要等待它?【英文标题】:Why do I need to await an async function when it is not supposedly returning a Promise?当异步函数不应该返回 Promise 时,为什么我需要等待它? 【发布时间】:2019-11-15 15:40:52 【问题描述】:

考虑这段代码:

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]

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

异步函数加载() 常量数据 = 等待新的承诺(解决 => setTimeout(() => 解决([1, 2, 3]), 10); ).then(data => data.map(i => i * 10)); console.log(`函数内的数据:$JSON.stringify(data)`); 返回数据; 异步函数 main() 常量数据 = 等待 load(); console.log(`加载的数据:$JSON.stringify(data)`); 主要的();

我会得到这个:

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

我很困惑,因为基于the documentation,await 应该暂停执行,直到承诺得到解决。在这种情况下,第一个示例应将 data 作为数组返回。但正如你所见,它返回了一个Promise,我不知道为什么!?

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

await 可以拆分执行流程,允许 await 的调用者 函数在延迟继续之前恢复执行 等待的功能。在等待推迟其继续之后 函数,如果这是该函数执行的第一个等待, 立即执行也继续返回到函数的 调用者等待完成等待功能的 Promise 并恢复该调用者的执行。

在我看来,await 仅在代码中的 所有 函数都是 async 时才有效,这很荒谬,因为如果我使用来自另一个模块的函数,应该如何我知道是不是async!?或者,也许我应该谨慎行事,始终使用await 调用所有函数,无论它们是否为async!!!!

[更新]

感谢所有参与并为我提供见解的人。但我仍然很困惑我应该如何使用awaitasync。我是否应该始终使用await 调用我的所有函数?

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

【问题讨论】:

关于您的编辑:在任何不是 Promise 的东西上使用 await 是没有意义的。大概当你从库中调用一个函数并尝试使用它的返回值时,你已经知道它是否返回一个 Promise。该库的文档会告诉你,你很难在不知道它是否返回 Promise 的情况下发现一个函数根本存在。 当你调用一个返回 Promise 的函数时,你可以使用 await,但不是必须的。你可能也想直接使用 Promise 对象 这部分我理解并且我接受我需要知道我调用的任何函数的返回值。在这一点上,我在实现asyncawait 时遇到的问题更像是一个好习惯。您是否总是假设您将来编写的代码将始终返回Promise?如果不是,如果一些应该是同步的代码结果是异步的会发生什么?您需要返回应用程序的入口点并等待所有调用,对吗?你如何避免这样的变化? 您通常在编写代码之前就知道您的代码是否需要等待异步的东西,如果不需要,那么以后不太可能需要。如果您确实需要将现有接口从同步更改为异步,那么您是正确的,它确实对每个调用者和调用者的调用者等产生了一种级联雪崩效应。常规回调函数也是如此。不过,这似乎不太可能。我不相信我必须将现有接口从同步更改为异步。 你很幸运 :) 这就是我过去几个小时所做的事情 【参考方案1】:

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

这个承诺最终会用你从异步函数返回的任何值来解决。

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

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

仅供参考,您的 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() 函数通常在很多你想延迟承诺链的地方很有用。


感谢所有参与并为我提供见解的人。但我仍然很困惑我应该如何使用等待和异步。

首先,大多数情况下,如果您需要在函数内部使用await,您只需标记函数async

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

这里有几个使用 async/await 的充分理由示例:

对多个异步操作进行排序

假设您有 getFromDatabase()getTheUrl()getTheContent(),它们都是异步的。如果有任何失败,您可能只想用第一个错误拒绝返回的承诺。

这是没有 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 版本的优势就越多。

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

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

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

而且,如果您使用 await 并且您正在等待的任何 promise 都被拒绝,并且您在 promise 上没有 .catch() 并且周围没有 try/catch,那么 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;        

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

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

在一个正常的函数中,如我们之前所说的那样:

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() 捕获所有可能的错误,他们必须用try/catch 包围它以捕获同步异常并使用.catch() 来捕获被拒绝的promise:

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

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

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 的库或者它是一个异步函数,我是否应该将我的所有函数调用从异步点追溯到应用程序的入口点,并在所有函数调用之前添加一个 await 在使它们异步之后?

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

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

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

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

    await 也仅在有特定原因时使用。我不只是在每个返回承诺的函数上自动使用它。我已经描述了使用它的上述原因。仍然可以使用.then() 来处理返回承诺的单个函数调用的结果。在某些情况下,使用 .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));

【讨论】:

我的load() 函数的重点是执行一些异步操作并使用await 使其同步 @Mehran - 你不能在 Javascript 中做到这一点。您永远不能获取异步获取的值并同步返回它。 Async/await 不这样做。 Javascript 中没有任何东西可以做到这一点。您总是会从 async 函数中获得返回的承诺,并且在该函数中不使用 await 会改变这一点。请阅读我的答案的详细信息,以更好地了解async 函数的工作原理。 @jfriend00 你可以同步返回一个异步值。 (例如 npm 中的同步 rpc)。没有理由这样做。 @Paulpro - 好吧,这似乎使用外部代码来明显阻止整个 Javascript 线程,这也可以使用 child_process 中的 sync api 来完成,但这些都不是我们真正想要的说到这里。 @Paulpro 但是你不能在 javascript 中做到这一点仍然是真的 - 你需要在 C 中做到这一点(你可以在 node.js 中做到,但是使用节点的 C API 而不是节点的 javascript API)【参考方案2】:

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

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

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

另外,如果一个函数是async,你可以await在它里面的任何promise(包括另一个async函数的结果)并且函数代码的执行将在await暂停直到那个promise已解决或被拒绝。

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

【讨论】:

我并不是要粗鲁,但您多久阅读一次库的代码以查看您使用的函数是否为async 我不同意 async/await 不会为该语言带来新功能。它们允许您以比以前更简单的方式编写异步代码,特别是在对多个异步操作进行排序时。虽然没有它们你确实可以编写任何东西,但通常你也可以在没有 Promise 的情况下编写任何东西,但 Promise 也为该语言带来了许多新功能。 @Mehran - 你需要做的就是查看函数的接口,看看它是否返回一个承诺。您实际上并不关心是否自动返回 promise,因为它被标记为 async 函数或是否手动返回 promise。无论哪种方式,接口都是相同的——调用者处理返回的承诺。 我完全同意 awaitasync 使您的代码更具可读性。我要说的是,它们不是应有的样子。在我看来,它们的实施是错误的。 @Mehran - 好吧,这只是你的意见。它们是非常有用的工具,可以使异步编码更易于编码、阅读和维护。【参考方案3】:

因为第一个函数是异步的 - 所以它在 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,你必须使用asynchronously,因为它总是返回一个Promise。

【讨论】:

以上是关于当异步函数不应该返回 Promise 时,为啥我需要等待它?的主要内容,如果未能解决你的问题,请参考以下文章

c++中的异步编程——future,promise

当循环在异步函数内部而不是相反时,为啥 Async/Await 可以正常工作?

为啥 Body.json() 返回一个 Promise? [复制]

react-native async 函数返回 promise 但不返回我的 json 数据?

为啥这个异步函数在它之前定义的等效 Promise.then 链之前执行?

vue+webpack2实现路由的懒加载