vlambda博客
学习文章列表

Async/await:让异步编程更简单

介绍

async functions  和  await  关键字是最近添加到JavaScript语言里面的。它们是ECMAScript 2017 JavaScript版的一部分(参见 ECMAScript Next support in Mozilla )。简单来说,它们是基基于promises的语法糖,使异步代码更易于编写和阅读。
  • Async/Await 是一个编写异步代码的新方式。之前异步代码的替代方案有回调和 promises。

  • Async/Await 其实只是一个建立在 promises 之上的一个语法糖。它不能同普通回调或者 node 回调一起使用。

  • Async/Await 跟 promises 一样,不会阻止代码往下执行。

  • Async/Await 使得异步代码不论看起来还是行为上都有点像同步代码,这正是它厉害的地方。


语法

假设我们想获取某个网址并以文本形式记录响应日志。以下是利用 Promise 编写的代码
function logFetch(url) { return fetch(url) .then(response => response.text()) .then(text => { console.log(text);      return "done"; }).catch(err => { console.error('fetch failed', err); });}
logFetch();

以下是利用async/await具有相同作用的代码:

async function logFetch(url) { try { const response = await fetch(url); console.log(await response.text()); return "done"; } catch (err) { console.log('fetch failed', err); }}
logFetch();

对比代码可以看到不同之处:

  1. 我们的函数前面有一个 async 关键字。await 关键字只能在同 async 一起定义的函数内部使用。每一个 async 函数隐式地返回一个 promise,然后这个 promise resolve 的将会是任意从那个函数的 return 返回值(在我们的例子中就是字符串 "done")。

  2. 上面那一点表明我们无法在我们代码的顶层使用 await,因为不在 async 函数内部。


// 这运行不了// await logFetch()
// 这个可以运行logFetch().then((result) => { // do something})

3. await fetch() 意味着在 fetch() 返回的 promise 被 resolve 之后才会调用 console.log 打印值。


为什么 async/await 更好?

Async/await:让异步编程更简单


 1. 简明清晰

我们不必写 .then,创建一个匿名函数来处理返回结果,或者给一个不必使用的变量叫 data。我们也可以避免嵌套代码。
2. 错误处理

sync/await 使得在同一个代码块中同时处理同步和异步错误成为可能,对于 try/catch 有利。在下面使用 promises 的例子中,try/catch 无法处理 JSON.parse 的失败,因为它在 promise 内部。我们需要在 promise 上面调用 .catch,重复处理错误,这比在你的预生产代码里 console.log 要复杂得多。

const logFetch = (url) => { try {    fetch(url) .then(result => { // this parse may fail const data = JSON.parse(result) console.log(data) }) // uncomment this block to handle asynchronous errors // .catch((err) => { // console.log(err) // }) } catch (err) { console.log(err) }}

现在来看看使用 async/await 的代码,catch 代码块现在可以处理解析错误了。

const logFetch = async (url) => { try { // this parse may fail const data = JSON.parse(await fetch(url)) console.log(data) } catch (err) { console.log(err) }}


3. 条件判断

想象下如下面这样的代码,获取一些响应日志的数据,然后决定是否应该直接返回还是在已有的值的基础上获得更多的细节。
const logFetch = (url) => { return fetch(url) .then(data => {      if (data.needsAnotherFetch) {        return logAnotherFetch(data) .then(moreData => { console.log(moreData); return moreData; }) } else { console.log(data); return data; } })}

这看起来相当头痛,很容易在嵌套里看晕(6层嵌套),返回语句只需将最终结果给主 promise。

下面这个使用 async/await 写的例子的可读性就好得多。
const logFetch = async (url) => { const data = await fetch(url); if (data.needsAnotherFetch) {    const moreData = await logAnotherFetch(data); console.log(moreData); return moreData; } else { console.log(data); return data;  }}

4. 中间值(媒介值)

有这样一个场景,你调了 promise1 之后使用它的返回值来调 promise2,然后使用它们返回的结果来调 promise3。
const logFetch = () => { return promise1() .then(value1 => { // do something return promise2(value1) .then(value2 => { // do something  return promise3(value1, value2); }) })}

如果 promise3 不需要 value1 的话就会容易将 promise 拉平一点。如果想代码简化一点, 可以使用Promise.all 将 value 1 和 value 2 包裹,这样可以避免深层次的嵌套。如下所示:

const logFetch = () => { return promise1() .then(value1 => { // do something return Promise.all([value1, promise2(value1)]); }) .then(([value1, value2]) => { // do something  return promise3(value1, value2); })}

这种方式为了可读性牺牲了语义性。因为没有必要让 value1 和 value2 一起放在一个数组里,除了避免 promises 的嵌套。

当使用 async/await 优化时,如下所示:

const logFetch = async () => { const value1 = await promise1(); const value2 = await promise2(value1);  return promise3(value1, value2);}

5. 错误堆栈

这样一个场景,在一个promise 链中,在后面的某处发生了错误。

const logFetch = () => { return callAPromise() .then(() => callAPromise()) .then(() => callAPromise()) .then(() => callAPromise()) .then(() => callAPromise()) .then(() => { throw new Error("oops"); })}
logFetch() .catch(err => { console.log(err); // output // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13) })

从 promise 链返回的错误堆栈信息无从知道错误到底发生在哪儿

使用async/await函数

const logFetch = async () => { await callAPromise(); await callAPromise(); await callAPromise(); await callAPromise(); await callAPromise(); throw new Error("oops");}
logFetch() .catch(err => { console.log(err); // output // Error: oops at logFetch (index.js:7:9)  });

本地开发时可能作用不大,但是对于你排查生产环境的问题时优势就比较明显。在这些状况下,知道错误时发生在 logFetch 比只知道错误在一个又一个 then 后面要好得多。


6.  Debugging 

最后这个特点并不是最不重要的一个。使用 async/await 的一个杀手锏是它很容易进行 debug。在 promises 中进行 debug 很痛苦有两个原因:
  1. 你无法在箭头函数的返回表达式中设置断点(无函数体)。

const logFetch = async () => { return callAPromise()      .then(() => callAPromise())   .then(() => callAPromise())   .then(() => callAPromise())      .then(() => callAPromise())}

2. 当你在 .then 里面打断点时,断点并不会移动到紧邻的下一个 .then 里面去,因为它只会在同步代码中移动断点。

而使用 async/await 时,你就不必使用箭头函数了,而且你可以一步一步的执行 await 调用,因为它们就是普通的同步代码。

const logFetch = async () => { await callAPromise(); await callAPromise(); await callAPromise(); await callAPromise(); await callAPromise();}

async/await的缺陷

Async/await 让你的代码看起来是同步的,在某种程度上,也使得它的行为更加地同步。await 关键字会阻塞其后的代码,直到promise完成,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但您自己的代码被阻塞。
这意味着您的代码可能会因为大量await的promises相继发生而变慢。每个await都会等待前一个完成,而你实际想要的是所有的这些promises同时开始处理(就像我们没有使用async/await时那样)。

有一种模式可以缓解这个问题——通过将 Promise 对象存储在变量中来同时开始它们,然后等待它们全部执行完毕。

function timeoutPromise(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ resolve("done"); }, interval); });};
let startTime = Date.now();timeTest().then(() => { let finishTime = Date.now(); let timeTaken = finishTime - startTime; alert("Time taken in milliseconds: " + timeTaken);});

第一个示例:

async function timeTest() { await timeoutPromise(3000); await timeoutPromise(3000); await timeoutPromise(3000);}

在这里,我们直接等待所有三个timeoutPromise()调用,使每个调用3秒钟。整个调用完成花费9秒。

第二个示例:

async function timeTest() { const timeoutPromise1 = timeoutPromise(3000); const timeoutPromise2 = timeoutPromise(3000); const timeoutPromise3 = timeoutPromise(3000);
await timeoutPromise1; await timeoutPromise2; await timeoutPromise3;}

在这里,我们将三个Promise对象存储在变量中,这样可以同时启动它们关联的进程。

接下来,我们等待他们的结果 - 因为promise都在基本上同时开始处理,promise将同时完成;当您运行第二个示例时,您将看到弹出框报告总运行时间仅超过3秒!

总结


Async/await 是近几年  JavaScript  新增的最具有革命性的特性之一了,它让你意识到 promises 有多么混乱,并且提供了一个直观的替代品。 Async/Await 如此优于 Promises ,赶快用起来吧。

参考文章:
《  异步函数 - 提高 Promise 的易用性  》-- google developers
《  asy nc和await:让异步编程更简单  》 -- MDN
《  6 R easons Why JavaScript Async/Await Blows Promises Away (Tutorial) 》
《  ECMAScript 6 入门    -- 阮一峰