NodeJS 从 7.6 版本开始已经内置了对 async/await 的支持,如果你错过了,还没用过该特性,这里有一些原因,来说明为什么你应该立即采用它。
Async/await 101
对于那些从未听说过这个话题的人来说,这是一个简单的介绍:
- Async/await 是编写异步代码的新方式。以前的异步代码选项是回调和 promise。
- Async/await 实际上是建立在 promise 之上的。它不能与普通回调或 node 回调一起使用。
- Async/await,和 promise 相似,不能阻塞。
- Async/await 使异步代码看起来更像一个同步代码。这就是它所有的能力所在。
语法
假设函数 getJSON 返回 Promise,而这个 Promise 由某个 JSON 对象兑现。我们想做的只是调用这个函数,记录 JSON,然后返回”done”。 使用 Promise 实现如下
const makeRequest = () => getJSON() .then(data => { console.log(data) return "done" }) makeRequest()
使用 async/await 实现如下
const makeRequest = async () => { console.log(await getJSON()) return "done" } makeRequest()
它们的区别在于
- 我们的函数前有 async 关键字。await 关键字只能在定义为 async 的函数中使用。所有 async 函数都会隐式地返回 Promise,而函数的返回值将作为 Promise 兑现的值 (本例中是字符串 “done”)。
- 上一条意味着我们不能在顶层代码中使用 await,因为那不在任何 async 函数中。
// this will not work in top level // await makeRequest() // this will work makeRequest().then((result) => { // do something })
- await getJSON() 意思是 console.log 会在 getJSON() 返回的 Promise 兑现出返回的值时再进行打印。
为什么它更好呢?
1. 简明整洁
看看我们并没有写多少代码! 即使在上面的例子中,也很清楚明了,我们节省了大量的代码。 我们不必编写.then,不必创建一个匿名函数来处理响应,不必将命名数据传递给我们不需要使用的变量。我们也避免了嵌套代码。 这些小的优点叠加起来,这在下面的代码示例中将会变得更加明显。
2. 错误处理
Async/await 使得用同一种构造(古老但好用的 try/catch ) 处理同步和异步错误成为可能。在下面的 promises 示例中,因为错误发生在 promises 中,try/catch 将不会处理 JSON.parse 失败的情况。我们需要在 promise 中调用 .catch ,并复制我们的错误处理代码,这将可能比你的生产就绪代码中的 console.log 更复杂。
const makeRequest = () => { try { getJSON() .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 模块现在可以处理 parsing 错误了。
const makeRequest = async () => { try { // this parse may fail const data = JSON.parse(await getJSON()) console.log(data) } catch (err) { console.log(err) } }
3. Conditionals
设想一下像下面的代码,它会获取一些数据,并决定是否应该返回该数据,或者根据数据中的某些值获取更多信息。
const makeRequest = () => { return getJSON() .then(data => { if (data.needsAnotherRequest) { return makeAnotherRequest(data) .then(moreData => { console.log(moreData) return moreData }) } else { console.log(data) return data } }) }
仅仅是看着这些就让你头疼了。很容易迷失在所有嵌套中(6级),大括号和返回语句中,而这些仅仅是在将最终结果传递给主 promise 中所需要的。 当用 async/await 重写时,此示例变得更易于阅读。
const makeRequest = async () => { const data = await getJSON() if (data.needsAnotherRequest) { const moreData = await makeAnotherRequest(data); console.log(moreData) return moreData } else { console.log(data) return data } }
4. 中间值
你可能会遇到这样一种情形,先调用 promise1,其返回值将用于调用 promise2,之后再调用 promise3 的时候需要用到前面两个返回值。那么代码可能写成这样
const makeRequest = () => { return promise1() .then(value1 => { // do something return promise2(value1) .then(value2 => { // do something return promise3(value1, value2) }) }) }
如果 promise3 不需要 value1,那么 Promise 就不需要嵌套这么深。如果你是不喜欢深层嵌套的人,可以使用 Promise.all 来封装 value1 和 value2 来避免深层嵌套,像这样:
const makeRequest = () => { return promise1() .then(value1 => { // do something return Promise.all([value1, promise2(value1)]) }) .then(([value1, value2]) => { // do something return promise3(value1, value2) }) }
这种方法会牺牲语义,降低可读性。把 value1 和 value2 放在一个数组里,除了避免嵌套 Promise,找不到其它任何理由。 使用 async/await 来完成同样的逻辑就非常简单。它会让你怀疑之前为简化 Promise 而进行的所有艰苦奋斗。
const makeRequest = async () => { const value1 = await promise1() const value2 = await promise2(value1) return promise3(value1, value2) }
5. 错误栈
想象一下,某段链式调用的代码中使用了多个 Promise,其中的某个位置会抛出一个错误。
const makeRequest = () => { return callAPromise() .then(() => callAPromise()) .then(() => callAPromise()) .then(() => callAPromise()) .then(() => callAPromise()) .then(() => { throw new Error("oops"); }) } makeRequest() .catch(err => { console.log(err); // output // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13) })
从 Promise 链中返回的错误栈找不到一点错误发生位置的线索。更糟糕的是,它会产生误导;它的包含的唯一的函数名是 callPromise,但它与这个错误无关 (文件和行号仍然有用)。 不过,async/await 产生的错误栈会指向包含错误的函数
const makeRequest = async () => { await callAPromise() await callAPromise() await callAPromise() await callAPromise() await callAPromise() throw new Error("oops"); } makeRequest() .catch(err => { console.log(err); // output // Error: oops at makeRequest (index.js:7:9) })
在本地环境中开发并使用编辑器编辑文件的时候这并不是多大的好处,不过如果你想从生产服务器获取错误日志,这就非常有用了。这种情况下,知道错误发生在 makeRequest 中总比不知道它在若干 then 调用的哪一个里面好吧 …
6. 调试
最后一点并非最不起眼,async/await 的杀手锏是易于调试。调试 Promise 总会有 2 点特别痛苦 1. 不能为箭头函数表达式设置断点 (没有函数体)。 (找个地方设置断点试试) 2. 如果在 .then 块中设置断点,然后使用调试快捷方式,比如跳过,调试器并不会移到下一个 .then 中,因为它是在代码中“步进”。 有了 async/await,你就不再需要这么多箭头函数,你可以在 await 调用中步进,就像普通的同步代码调用那样。
小结
async/await 是近几年来 JavaScript 引入的最具革命性的特性之一。它让人意识到 Promise 给语法带来的混乱,并提供了直观的替代方案。
相关事项
你在使用这个特性的时候可能会产生一些疑问
- 它使异步代码不那么显眼:我已经习惯通过看到的回调或 .then 来判断异步代码,现在需要几周时间让眼睛习惯注意到新的迹象,不过 C# 拥有这个特性已经有几年了,所以熟悉这个特性的人知道这个短暂的小小的不便是值得付出的代价。
- Node 7 并非 LTS(Long Time Support,长期支持) 版本:是的,不过下个月 Node 8 就来了,(现在使用的话),迁移到新版本就会非常轻松甚至不需要任何改变。
英文原文:6 Reasons Why JavaScript’s Async/Await Blows Promises Away
转自 https://www.oschina.net/translate/6-reasons-why-javascripts-async-await-blows-promises-away