异步/等待与then,哪个对性能更好?

118

我有一段简单的JavaScript代码,可以执行API请求并返回响应。然而,在这种情况下,我将会有成千上万个请求。那么,在这两种代码选项中,哪一个的性能更好,为什么?同时,哪一个是现在推荐的良好实践?

第一种选择是使用.then来解析promises,第二种选择是使用async/await。

在我的测试中,这两个选项的结果非常相似,没有显着的差异,但我不确定在规模上会怎样。

// Using then
doSomething(payload) {
  const url = 'https://link-here/consultas';
  return this.axios.get(url, {
    params: {
      token: payload.token,
      chave: payload.chave,
    },
   }).then(resp => resp.data);
}

// Using Async / await
async doSomething(payload) {
   const url = 'https://link-here/consultas';
   const resp = await this.axios.get(url, {
   params: {
     token: payload.token,
     chave: payload.chave,
    },
 });
 return resp.data;
}

任何解释都会非常有价值。


2
嗨!请看一下 https://blog.pusher.com/promises-async-await/ 。别忘了用 try/catch 块包装你的 async/await 代码。 - Nikolay Vetrov
7
性能差异实际上不应该成为一个让人担忧的问题:毕竟,HTTP 请求占据了执行时间的 >99%。如果您可以并行启动几个请求,并使用 Promise.all 合并它们的响应,则可以获得真正的收益。然后启动下一批,依此类推... - trincot
4
我不确定这是否适用于此处,但请参考来自V8开发人员的文章结尾中的一般建议:“优先使用async函数和await,而非手写的Promise代码”。 - vsemozhebuty
3
在这里用try/catch包装await没有必要。原帖作者只是想要返回被拒绝的 promise,而async函数会自动捕获拒绝并执行此操作。在比这更复杂的错误处理情况下需要并且有用的是try/catch - jfriend00
8个回答

187
从性能角度来看,await只是.then()的内部版本(基本上做同样的事情)。选择其中一个而不是另一个的原因与性能无关,而是与所需的编码风格或编码方便有关。当然,解释器有更多优化内部内容的机会使用await,但这不太可能是你决定使用哪个的原因。如果其他条件都相等,我会选择await,因为它可以简化代码编写、理解和维护。
正确使用await通常可以节省大量代码行,使代码更易于阅读、测试和维护。这就是它被发明的原因。
在您的代码的两个版本之间没有实质性的区别。当axios调用成功或出错时,两者都会达到相同的结果。 await可以在需要串行化多个连续异步调用的情况下更方便地使用。然后,您可以只使用await而不是将它们每个放在.then()处理程序中以正确链接它们,这样代码看起来更简单。
使用await.then()时常见的错误是忘记正确的错误处理。如果您在此函数中的错误处理意图只是返回被拒绝的Promise,则两个版本都完全相同。但是,如果您有多个异步调用,并且想要执行比仅返回第一个拒绝更复杂的操作,则await.then()/.catch()的错误处理技术是非常不同的,哪种方法更简单将取决于具体情况。请保留原文中的占位符

1
正确。使用“await”的另一个好处是函数被明确标记了。 - Tal L
很好的回答。作为一个新手,我发现异步编程范式中最令人困惑的是当两种风格混合使用时,有些是.then,有些是await,但我猜这只是一种布局或风格上的选择?例如,这个示例代码:https://github.com/databricks/databricks-sql-nodejs/blob/55556890c022062dec5f2243ec7b9bae7f9c06ac/examples/session.js#L11 - Davos
3
@Davos - 这并不是一种风格选择。通常不建议混合使用await.then(),因为这只会使代码更加复杂难懂。如果可以使用await,通常它将是更简单易懂的代码。 - jfriend00
1
@maddy - await 本身并不会阻塞 UI。await也不会阻塞JS解释器。如果你使用await等待某些操作,直到 await 后再进行渲染,那么 UI 就会等到 Promise 解决之后才会渲染,但这是由于你的代码逻辑引起的,并非await造成的。如果你在 .then() 处理程序中渲染,也会发生同样的事情。无论哪种方式,都必须在异步操作完成之前等待才能进行渲染,这与 await vs. then() 没有任何关系。这完全取决于你的代码是如何编写的。 - jfriend00
谢谢分享这些宝贵的见解,我还需要进一步理解。在我的应用程序中,当我使用await时,我注意到在导航到新屏幕时会出现视觉上的延迟,而一旦我将我的API调用更改为then,我就能够体验到非阻塞的用户交互。 - maddy
显示剩余2条评论

52

这个主题需要进行一些修正。使用await.then将会得到非常不同的结果,应该出于不同的原因使用。

await会等待某些事情完成,然后继续执行下一行代码。它也比另一个更简单,因为它机械地表现得更像同步行为。你做第一步,等待,然后继续执行。

console.log("Runs first.");
await SomeFunction();
console.log("Runs last.");

.then会从原始调用中分离出来,并在其自己的作用域内开始操作,它将在原始作用域无法预测的时间更新。如果我们可以暂时放下语义问题,那么它是“更加异步”的,因为它离开了旧的作用域并分支到一个新的作用域。

console.log("Runs first.");
SomeFunction().then((value) => {console.log("Runs last (probably). Didn't use await on SomeFunction().")})
console.log("Runs second (probably).");

10
为什么使用await/.then会得到不同的结果,它们的设计目的只是为了用不同的语法完成相同的任务。 其次,技术上讲,await并不具有阻塞行为,因为它不会阻塞线程。 我也不会将.then描述为“更加异步”。它们都同样是异步的,我认为这样考虑并没有什么帮助。 - pushkin
请在您的回答中提供更多细节。目前的写法让人难以理解您的解决方案。 - Community
2
我添加了一些澄清来说明我的意思。这是两个非常不同的函数。显然,async需要更多的讨论,这应该在完整的指南中进行介绍,并且我明确地在语句前加上了“如果我们不考虑语义”的前缀。 - user280209
你在评论问题中根本没有被问到的内容。这个问题纯粹是关于选择一个而不是另一个的性能原因,而不是所有其他编码原因或所有其他差异。看看操作员的两个代码示例。他们只是询问这些之间的区别,没有更多的要求。你的回答并不是错误的信息,但它并不是对所问问题的回答。 - jfriend00
@jfriend00 他明确表示then创建了一个新的作用域。我想他可能会详细阐述底层细节,但是创建作用域需要内存(和管理)。因此,仅基于这一点,就可以基本确定 then 的性能不如 await,特别是在循环的情况下,使用 then 必须使用递归,这意味着所有的 promise 在循环结束之前都将保留在内存中。而异步版本的该循环则不会存在同样的问题。 - gabriel.hayes

36

作为对@user280209答案的更多解释,让我们考虑以下返回promise的函数,并将其执行与.then()async await进行比较。

function call(timeout) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`This call took ${timeout} seconds`);
      resolve(true);
    }, timeout * 1000);
  });
}

使用.then()

(async () => {
  call(5).then((r) => {
    console.log(r);
  });
  await call(2); //This will print result first
  await call(1);
})();

运行上述调用时,日志将会是:

This call took 2 seconds
This call took 1 seconds
This call took 5 seconds
true

我们可以看到.then()没有暂停其下一行的执行直到它完成。

使用async/await

(async () => {
  await call(5); //This will print result first
  await call(2); 
  await call(1);
})();

运行以上函数时,日志将会被记录。

This call took 5 seconds
This call took 2 seconds
This call took 1 seconds

所以我认为,如果你的 Promise 结果不会在接下来的代码中使用,.then() 可能是更好的选择。


2
谢谢提供示例!在第一个 call(5).then 中,当您等待 await call(2)await call(1) 时,它的执行是否会在后台继续进行? - Doug
虽然这些信息并不是不准确的,但它们与这里所问的性能问题有什么关系呢?我甚至没有看到你对 OP 的两个代码块以及它们之间的性能差异发表任何评论。这才是这里所问的问题。 - jfriend00
4
@jfriend00在你的答案中倒数第二段似乎暗示await和then有相同的行为,仅区别在于await打字更短。这个用户280209的回答澄清了它们之间的明显差异。虽然这与问题并没有直接关系,但添加这一点可以提供隐含重要信息来更好地理解await和then的区别。 - Juan Perez
如果您只有一个异步操作,就像代码中所示的那样,那么使用.then()并返回Promise或使用await没有区别。无论哪种方式都会得到相同的结果,因此这只是个人编码偏好。正如我在答案中所说,当需要按顺序运行多个异步操作时,await具有编码简单性优势。 - jfriend00
4
值得一提的是,.then() 示例的墙钟时间为3秒,而async示例的墙钟时间为8秒。换句话说,每个await都会暂停线程并字面上“等待”结果,而.then()则以异步方式运行。 - Ben
@Ben 这就是我对用户体验的感受和经历。 - maddy

8

对于那些认为await会阻塞代码直到异步调用返回的人来说,你们忽略了关键点。"await"只是一个promise.then()的语法糖。它实际上将函数的剩余部分封装在它所创建的promise的then块中。没有真正的"阻塞"或"等待"。

run();

async function run() {
    console.log('running');
    makePromises();
    console.log('exiting right away!');
}

async function makePromises() {
    console.log('make promise 1');
    const myPromise = promiseMe(1)
    .then(msg => {
        console.log(`What i want to run after the promise is resolved ${msg}`)
    })
    console.log('make promise 2')
    const msg = await promiseMe(2);
    console.log(`What i want to run after the promise is resolved via await ${msg}`)
}   

function promiseMe(num: number): Promise<string> {
    return new Promise((resolve, reject) => {
        console.log(`promise`)
        resolve(`hello promise ${num}`);
    })
}

在makePromises函数中的await语句不会阻塞任何内容,输出结果为:

  • 运行
  • 创建promise 1
  • promise
  • 创建promise 2
  • promise
  • 立即退出!
  • 我想在promise解决后运行的代码 hello promise 1
  • 我想通过await在promise解决后运行的代码 hello promise 2

3
如果使用await将函数的尾部放在.then块中,它确实会导致不同的行为。 - Siavoshkc
1
只需在makePromises()函数中更改Promise2(async)和Promise1(then)的创建顺序,您将看到await确实会阻塞。然后您将得到以下输出: running; make promise 2; promise; exiting right away!; What i want to run after the promise is resolved via await hello promise 2; make promise 1; promise; What i want to run after the promise is resolved hello promise 1;请注意,“make promise 1”直到promise 2被解决才会被打印。 - 2020
请注意,当您交换promise2和promise1时,“make promise 1”直到promise2被解决才会打印出来。当我们说await阻塞时,它仅阻塞发生await的函数,即makePromises函数而不是“run”函数! - 2020

5
实际上,使用Await/Async可以更有效地执行,因为Promise.then()在执行后会失去调用它的作用域,你正在将回调附加到回调栈中。 导致的问题是:系统现在必须存储.then()被调用的位置的引用。如果发生错误,则必须正确指向错误发生的位置,否则,由于没有作用域(系统在调用Promise后恢复执行,等待稍后返回到.then()),它无法指向错误发生的位置。 使用Async/Await时,您暂停了调用该方法的执行,从而保留了引用。

4

如果我们只考虑性能(所需时间),那么它实际上取决于您的操作是串行还是并行。如果您的任务是串行的,那么使用await和.then之间将没有区别。但是,如果您的任务是并行的,那么.then将需要更少的时间。考虑以下示例:

let time = Date.now();

// first task takes 1.5 secs 
async function firstTask () {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1);
        },1500)
    })
}

// second task takes 2 secs 
async function secondTask () {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2);
        },2000)
    })
}

// using await
async function call(){
    const d1 = await firstTask();
    const d2 = await secondTask();
    console.log(Date.now()-time, d1+d2)
}
call()


// using .then
async function call2(){
    let d1=null,d2=null;
    firstTask().then(data => {
        d1=data;
        if(d2){
            console.log(Date.now()-time, d1+d2);
        }
    })
    secondTask().then(data => {
        d2=data;
        if(d1){
            console.log(Date.now()-time, d1+d2);
        }
    })
}
call2()

这里有两个任务,第一个任务需要1.5秒,第二个任务需要2秒。使用await进行调用的函数叫做call,而使用.then进行调用的函数叫做call2。输出结果如下:

From call2  2012 3
From call  3506 3

我希望这有所帮助。


1
使用await Promise.all([firstTask(),secondTask()]);进行并行执行,同时仍然使用await关键字。 - Zaeem Khaliq

3
据我所知,.then()和await不是同一回事。异步函数在实现上基本上就是生成器,因此在承诺被解决/拒绝之前,它不会执行下一个命令。相反,在.then()的情况下,函数的执行将继续进行下一个命令,并且当当前事件循环(关于这一点我不太确定)完成时,将执行解析/拒绝回调。

简而言之,对于单个承诺,await和.then()的行为类似,但当一个承诺需要另一个承诺先解决时,它们两者的行为完全不同。


0

这个问题已经有很多答案了。然而,为了指出上面答案中的关键信息和我的理解,请注意以下几点:

  • 只有在不处理错误返回时才使用 await
  • 如果没有关键需要进行错误处理,则使用 await
  • 如果返回的错误消息或数据对于调试/或适当的错误处理至关重要,请使用 .then .catch 而不是 try catch for await

从下面的代码示例中选择任何首选方法

const getData = (params = {name: 'john', email: 'ex@gmail.com'}) => {
  return axios.post(url, params);
}

// anywhere you want to get the return data

// using await

const setData = async () => {

  const data = await getData();
  
  }
  
  // to handle error with await
  const setData = async () => {
    try {
      const data = await getData();
      }
     catch(err) {
        console.log(err.message);
     }
  }
  
   // using .then .catch
  const setData = () => {
 
      var data; 
      getData().then((res) => { 
        data = res.data; console.log(data)
      }).catch((err) => {
        console.log(err.message);
      });

  }
  


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