`return await promise` 和 `return promise` 的区别是什么?(涉及IT技术)

218

在下面给出的代码示例中,是否存在行为上的区别?如果有,那么这些区别是什么?

return await promise

async function delay1Second() {
  return (await delay(1000));
}

返回promise

async function delay1Second() {
  return delay(1000);
}

据我所理解,第一个选项会在异步函数中进行错误处理,错误会从异步函数的Promise中传递出来。但是,第二个选项需要少一次刻度。这样说是否正确?

这段代码只是返回一个参考Promise的常见函数。

function delay(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

3
没错,我编辑了我的问题,因为你误解了我的意思,你的回答并没有真正回答我想知道的内容。请帮我将以下内容翻译成中文。 - PitaJ
1
@PitaJ:我相信你的意思是从第二个(return promise)示例中删除async - Stephen Cleary
1
@StephenCleary 不是的。我的意思是针对这个问题。想象一下在返回之前还有其他的await调用等等。 - PitaJ
2
@StephenCleary,我偶然发现这个问题,并且一开始的想法与你一样,一个被解析为另一个Promise的Promise在这里并没有什么意义。但事实证明,promise.then(() => nestedPromise)会展开和“跟随” nestedPromise。有趣的是,它与C#中的嵌套任务不同,我们需要使用 Unwrap 函数。顺便提一下,在 这里 看起来 await somePromise 调用了 Promise.resolve(somePromise).then,而不仅仅是 somePromise.then,具有某些有趣的语义差异。 - noseratio - open to work
显示剩余6条评论
7个回答

298
大多数情况下,returnreturn await没有明显的区别。两个版本的 delay1Second 行为是完全一样的(但是根据实现的不同,return await 版本可能会使用略多一些的内存,因为中间的一个 Promise 对象可能会被创建)。
然而正如 @PitaJ 指出的,存在一种情况有所区别:如果 returnreturn await 嵌套在 try-catch 块中。考虑以下示例。
async function rejectionWithReturnAwait () {
  try {
    return await Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

async function rejectionWithReturn () {
  try {
    return Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

在第一个版本中,异步函数在返回其结果之前等待被拒绝的 Promise,这会导致拒绝被转换为异常并达到 catch 子句;因此函数将返回解析为字符串 "Saved!" 的 Promise。

然而,在第二个版本的函数中,它直接返回拒绝的 Promise,而不是在异步函数内等待它,这意味着 catch 案例不会被调用,调用者将得到拒绝信息。


2
也许还应该提到即使没有try/catch,堆栈跟踪也会有所不同?我认为这是人们在这个例子中最常遇到的问题 :] - Benjamin Gruenbaum
我在一个场景中发现,在for...of循环内使用return new Promise(function(resolve, reject) { }),然后在pipe()之后的循环内调用resolve(),并不能像期望的那样暂停程序执行,但是使用await new Promise(...)可以。后者的语法是否有效/正确?它是return await new Promise(...)的“简写”吗?您能帮我理解为什么后者起作用而前者不起作用吗?为了背景,这种情况出现在此答案solution 02中。 - user1063287
这对于 finally 块也是成立的吧? - Ben Aston
最终块仍将运行,只是顺序不同。这些块将在承诺完成之前运行。 - Tamas Hegedus

32

正如其他答案所提到的,直接返回Promise可能会稍微提高性能,因为你不需要先等待结果,然后再用另一个Promise包装它。然而,还没有人谈论过尾调用优化。

尾调用优化,或称为“proper tail calls”,是解释器使用的一种优化调用栈的技术。目前,支持它的运行时不多,尽管它在ES6标准中已经被技术上确认,但未来可能会增加支持,因此您可以通过编写良好的代码来做好准备。

简而言之,TCO(或PTC)通过为直接由另一个函数返回的函数打开新的帧来优化调用栈。而是重复使用同一帧。

async function delay1Second() {
  return delay(1000);
}

由于delay()是由delay1Second()直接返回的,支持PTC的运行时将首先为delay1Second()(外部函数)打开一个帧,但是然后不会为delay()(内部函数)打开另一个帧,而是重复使用已经为外部函数打开的相同帧。这优化了堆栈,因为它可以防止非常大的递归函数(例如fibonacci(5e+25))产生堆栈溢出(嘿嘿),从而变成了循环,速度更快。

PTC仅在内部函数被直接返回时启用。如果在返回结果之前更改了函数的结果,例如,如果您有return (delay(1000) || null)return await delay(1000),则不会使用它。

但是像我说的那样,大多数运行时和浏览器现在还不支持PTC,所以现在可能没有太大的区别,但最好为将来保险起见。

在此问题中阅读更多信息:Node.js:异步函数中是否有尾调用优化?


await在典型的使用中实际上提供了更多的保护,防止堆栈溢出:如果一个递归的异步函数等待除了自身以外的任何东西,它将立即返回,意味着它的堆栈帧不会停留。实际上,堆栈上的命名值以与闭包相同的方式被保留(事实上,这是运行时的实现,意味着它在堆上)。考虑到只等待自身而没有实际的I/O或超时的异步函数不需要是异步的,所以它基本上是无用的,因此堆栈溢出不太令人担忧。 - Mack

26

显著的区别:Promise拒绝在不同位置被处理

  • return somePromise 将会将 somePromise 传递给调用栈,并且在调用栈上使用 await 等待 somePromise 的解决结果(如果有)。因此,如果 somePromise 被拒绝,它将不会被本地捕获块处理,而是被调用方的捕获块处理。

async function foo () {
  try {
    return Promise.reject();
  } catch (e) {
    console.log('IN');
  }
}

(async function main () {
  try {
    let a = await foo();
  } catch (e) {
    console.log('OUT');
  }
})();
// 'OUT'

  • return await somePromise 将首先等待 somePromise 在本地settled。因此,值或异常将首先在本地处理。 => 如果 somePromise 被拒绝,那么本地 catch 块将被执行。

async function foo () {
  try {
    return await Promise.reject();
  } catch (e) {
    console.log('IN');
  }
}

(async function main () {
  try {
    let a = await foo();
  } catch (e) {
    console.log('OUT');
  }
})();
// 'IN'

原因: return await Promise 同时在局部和外部都需要等待,return Promise 只在外部等待

详细步骤:

return Promise

async function delay1Second() {
  return delay(1000);
}
  1. 调用 delay1Second() 函数;
const result = await delay1Second();
  1. delay1Second()函数内,delay(1000)立即返回一个promise对象,其[[PromiseStatus]]值为'pending'。我们称之为delayPromise
async function delay1Second() {
  return delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. 异步函数将在Promise.resolve()内封装它们的返回值(来源)。因为delay1Second是一个异步函数,所以我们有:
const result = await Promise.resolve(delayPromise); 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. Promise.resolve(delayPromise) 返回 delayPromise 而不做任何事情,因为输入已经是一个promise对象(见MDN Promise.resolve):
const result = await delayPromise; 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. await 等待 delayPromise 被解决。
  • 如果 delayPromise 被 PromiseValue=1 这个值所满足:
const result = 1; 
  • 如果delayPromise被拒绝,则ELSE:
// jump to catch block if there is any

返回一个等待的 Promise

async function delay1Second() {
  return await delay(1000);
}
  1. 调用 delay1Second() 函数;
const result = await delay1Second();
  1. delay1Second()函数内部,delay(1000)函数立即返回一个Promise,并且[[PromiseStatus]]: 'pending',我们称之为delayPromise

async function delay1Second() {
  return await delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. 本地等待将会等待delayPromise被解决。
  • 情况1: delayPromise 被 PromiseValue=1 解决:
async function delay1Second() {
  return 1;
}

const result = await Promise.resolve(1); // let's call it "newPromise"

const result = await newPromise; 
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: 1

const result = 1; 
  • 情况2: delayPromise 被拒绝:
// jump to catch block inside `delay1Second` if there is any
// let's say a value -1 is returned in the end

const result = await Promise.resolve(-1); // call it newPromise

const result = await newPromise;
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: -1

const result = -1;

术语表:

  • Settle:当 Promise.[[PromiseStatus]]pending 变为 resolved 或者 rejected 时,表示 Promise 完成了状态转换。

讲解得非常清晰易懂!逐步包装和解包承诺的过程使差异变得一目了然。其中一个重要的收获是当传递一个承诺时,Promise.resolve返回的值。我最初认为它会返回一个已解决的承诺,但实际上它会原样返回承诺。 - Jamāl
虽然可能很小,但还是存在性能差异,使用return await更快。参见https://dev59.com/h1cQ5IYBdhLWcg3wFP2a#70979225。结合更好的错误处理,如果想要公开错误,应该始终使用`return await`并传递错误。 - ShortFuse

5
这是一个很难回答的问题,因为实际上它取决于你的转译器(可能是babel)如何渲染async/await。无论如何,以下几点是清楚的:
  • 两种实现应该是相同的,虽然第一种实现中可能会少一个 Promise 链。

  • 特别是如果你去掉不必要的 await,第二个版本不需要任何额外的代码从转译器中生成,而第一个版本则需要。

因此,从代码性能和调试的角度来看,第二个版本更可取,尽管只有非常微小的差别,而第一个版本具有稍微更好的可读性,因为它清楚地表明返回一个 promise。


为什么这两个函数的行为会相同呢?第一个返回一个已解决的值(undefined),而第二个返回一个 Promise - Amit
4
@Amit 两个函数都会返回一个 Promise。 - PitaJ
如果我将两个异步函数的主体都用 try-catch 包围起来会怎样呢?在 return promise 的情况下,任何 rejection 都不会被捕获,对吗?而在 return await promise 的情况下,它会被捕获,是吗? - PitaJ
@PitaJ - 假设你是对的,假设我想创建一个异步函数,它返回一个 Promise(不是包装在 Promise 中的值,而是 Promise 本身),以便稍后可以等待它。那么这个函数会是什么样子? - Amit
你只需要在async函数中稍后再不使用await - PitaJ
显示剩余10条评论

5
在我们的项目中,我们决定始终使用'return await'。 这样做的理由是,“在稍后将try-catch块放置在返回表达式周围时遗忘添加'await'的风险,证明现在拥有冗余的'await'是合理的。”

2
我完全同意。而且向新加入者解释“在调用异步函数时始终使用await,除非它立即返回,除非它在try-catch中”这一点是荒谬的。 - Tamas Hegedus

1

这是一个TypeScript的例子,您可以运行它并自己了解为什么需要加上“return await”关键字。

async function  test() {
    try {
        return await throwErr();  // this is correct
        // return  throwErr();  // this will prevent inner catch to ever to be reached
    }
    catch (err) {
        console.log("inner catch is reached")
        return
    }
}

const throwErr = async  () => {
    throw("Fake error")
}


void test().then(() => {
    console.log("done")
}).catch(e => {
    console.log("outer catch is reached")
});


1
我同意。看到一些受人尊敬的JS大师在StackOverflow上提倡相反的观点,真是太可悲了。 - Tamas Hegedus

0

在这里,我留下一些代码,让您能够理解差异。

 let x = async function () {
  return new Promise((res, rej) => {
    setTimeout(async function () {
      console.log("finished 1");
      return await new Promise((resolve, reject) => { // delete the return and you will see the difference
        setTimeout(function () {
          resolve("woo2");
          console.log("finished 2");
        }, 5000);
      });
      res("woo1");
    }, 3000);
  });
};

(async function () {
  var counter = 0;
  const a = setInterval(function () { // counter for every second, this is just to see the precision and understand the code
    if (counter == 7) {
      clearInterval(a);
    }

    console.log(counter);
    counter = counter + 1;
  }, 1000);
  console.time("time1");
  console.log("hello i starting first of all");
  await x();
  console.log("more code...");
  console.timeEnd("time1");
})();

函数"x"只是一个异步函数,它有其他的函数。如果删除返回值,它会打印"more code..."。

变量x只是一个异步函数,它又包含另一个异步函数。在代码主体中,我们调用等待来调用变量x的函数,当它完成后,它遵循代码的顺序,这对于"async/await"来说是正常的,但在x函数内部,还有另一个异步函数,它返回一个promise或者返回一个"promise",它将留在x函数内部,忘记了主要的代码,也就是说,它不会打印"console.log("more code..")",另一方面,如果我们加上"await",它将等待每个完成的函数,最后按照主代码的正常顺序进行。

在"console.log("finished 1")"下面删除"return",你会看到行为。


1
虽然这段代码可能解决了问题,但是包括解释它如何以及为什么解决了问题,将有助于提高您的帖子质量,并可能导致更多的赞。请记住,您正在回答未来读者的问题,而不仅仅是现在提问的人。请[编辑]您的答案以添加解释并指出适用的限制和假设。 - Brian61354270

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