ES2017在6月份已经定稿,随之而来的是我最喜欢的JavaScript特性将得到广泛的支持:async
函数。如果你以前使用JavaScript的异步函数遇到过困难,那这个就是为您准备的。如果你没有,那么,你可能是这方面的高手。
异步函数或多或少让你编写有顺序的JavaScript代码,而不需要在回调(callbacks)、生成器(generators)或者是Promise中包装所有逻辑。考虑一下下面的代码:
function logger() {
let data = fetch('http://sampleapi.com/posts')
console.log(data)
}
logger()
这段代码不符合你的预期。如果你用JS做了什么,你可能知道为什么。
但是这段代码确实做到了你所期望的。
async function logger() {
let data = await fetch('http:sampleapi.com/posts')
console.log(data)
}
logger()
这种直观(而且漂亮)的代码可以正常工作,而且只需要另外两个单词!
ES6之前的JavaScript异步函数
在深入了解async
和await
之前,你先要理解Promise。为了领会Promise,我们需要再多走一步,回到普通的回调中。
在ES6中引入Promise,并在JavaScript中编写异步代码时取得了很大的进步。再没有地狱般回调(Callback hell),有点亲切感。
回调是一个函数,它可以传递到函数中,并在函数内调用,以响应任何事件。这是JS的基础。
function readFile('file.txt', (data) => {
// 这在回调函数中
console.log(data)
}
该函数只是从一个文件中记录数据,这在文件完成读取之前是不可能的。这似乎很简单,但是如果你想要按顺序读取和记录五个不同的文件,又会怎么样呢?
在Promise之前,为了按顺序执行任务,你需要在回调中嵌套回调,就像这样:
// This is officially callback hell
function combineFiles(file1, file2, file3, printFileCallBack) {
let newFileText = ''
readFile(string1, (text) => {
newFileText += text
readFile(string2, (text) => {
newFileText += text
readFile(string3, (text) => {
newFileText += text
printFileCallBack(newFileText)
}
}
}
}
很难推理,很难跟上。这甚至不包括对完全可能的场景的错误处理,比如其中一个文件不存在。
有关于JavaScript中回调地狱更多的资料,可以阅读下面的文章: - 回调地狱的今生前世 - Node.js 异步最佳实践 & 避免回调地狱 - “回调地狱”如何避免 - 理解回调函数,回调地狱,Promise - 关于回调地狱 - Callback Hell
Promise会让它变得更好
这是Promise
可以提供帮助的地方。Promise是一种解释尚未存在的数据的方法,但你知道它会存在。《您不知道的JS》系列的作者@Kyle Simpson分享JavaScript异步函数而闻名。他对Promise的解释是:就像在快餐店里的食物一样。
- 你食物的顺序
- 给你的食物买单,并收到一张有订单号的票
- 等待你的食物
- 当你的食物准备好了,他们就会给你打电话
- 收到的食物
正如他所指出的,当你在等待的时候,你可能不能吃你的食物,但你可以考虑一下,你可以为此做好准备。你可以继续做你想做的事情,你知道食物将会到来,即使你还没有食物,因为承诺会给你食物。这就是promise
。表示最终会存在的数据的对象。
readFile(file1)
.then((file1-data) => { /* do something */ })
.then((previous-promise-data) => { /* do the next thing */ })
.catch( /* handle errors */ )
这就是promise
语法。它的主要好处是它允许一种直观的方式将连续事件链在一起。这个示例不错,但是你可以看到我们仍然在使用回调。promise
只是简单的回调,只是让回调看上去更为直观一些。
最佳方式:async / await
几年前,async
函数进入了JavaScript生态系统。截至上个月,它是该语言的官方特性,并得到了广泛支持。
async
和await
关键词是建立在Promise和generator上。本质上,它允许我们使用await
关键词暂停我们想要的任何地方的函数。
async function logger() {
// pause until fetch returns
let data = await fetch('http://sampleapi.com/posts')
console.log(data)
}
这段代码运行并做到你想要做的事情。它从API调用data
。如果你的大脑没有爆炸,我不知道该如何取悦你。
它的好处是非常直观。你编写代码的方式就是你大脑思考的方式,告诉脚本在需要的地方暂停。
另一个好处是你可以在不能实现的promise
中使用try
和catch
:
async function logger () {
try {
let user_id = await fetch('/api/users/username')
let posts = await fetch('/api/`${user_id}`')
let object = JSON.parse(user.posts.toString())
console.log(posts)
} catch (error) {
console.error('Error:', error)
}
}
这是一个人为的示例,但它证明了一点:catch
将获取过程中任何步骤中发生的错误。至少有三个地方的try
块可能会失败,这是迄今为止在异步代码中处理错误的最干净的方法。
我们还可以使用带有循环和条件的async
函数,而且不会令你感到头痛:
async function count() {
let counter = 1
for (let i = 0; i < 100; i++) {
counter += 1
console.log(counter)
await sleep(1000)
}
}
这是一个很愚蠢的例子,但它的执行将是你所期望的,而且代码还很容易阅读。如果你在控制台中执行此操作,你将看到代码将暂停sleep
调用,下一个循环代码不会在一秒中内启动。
细节
既然你已经确信了async
的美妙之处,那么就让我们深入了解一下细节吧:
async
和await
是建立在Promise之上的。使用async
的函数本身将始终返回一个Promise。记住这一点很重要,而且可能是你遇到的最大的“陷阱”- 当我们
await
时它会暂停函数,而不是整个代码 async
和await
是非阻塞的- 你依旧可以使用
Promise
的助手,比如Promise.all()
下面的代码是我们之前的例子:
async function logPosts () {
try {
let user_id = await fetch('/api/users/username')
let post_ids = await fetch('/api/posts/<code>${user_id}')
let promises = post_ids.map(post_id => {
return fetch('/api/posts/${post_id}')
}
let posts = await Promise.all(promises)
console.log(posts)
} catch (error) {
console.error('Error:', error)
}
}
await
只能用于已被声明为async
的函数- 不能在全局范围内使用
await
比如下面的代码:
// throws an error
function logger (callBack) {
console.log(await callBack)
}
// works!
async function logger () {
console.log(await callBack)
}
现在可以用
到2017年6月,几乎所有浏览器都可以使用async
和await
。更妙的是,要确保你的代码在任何地方都有效,请使用Babel来编译你的JavaScript代码,让更老的浏览器也能支持。
如果你对更多关于ES2017内容感兴趣,你可以浏览这个ES2017特性的完整列表。
本文根据@ERIC WINDMILL的《Using ES2017 Async Functions》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://css-tricks.com/using-es2017-async-functions/。
如需转载,烦请注明出处:https://www.w3cplus.com/javascript/using-es2017-async-functions.html