使用ES2017的异步函数

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()

async

这种直观(而且漂亮)的代码可以正常工作,而且只需要另外两个单词!

ES6之前的JavaScript异步函数

在深入了解asyncawait之前,你先要理解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生态系统。截至上个月,它是该语言的官方特性,并得到了广泛支持。

asyncawait关键词是建立在Promise和generator上。本质上,它允许我们使用await关键词暂停我们想要的任何地方的函数。

async function logger() {
    // pause until fetch returns
    let data = await fetch('http://sampleapi.com/posts')
    console.log(data)
}

这段代码运行并做到你想要做的事情。它从API调用data。如果你的大脑没有爆炸,我不知道该如何取悦你。

它的好处是非常直观。你编写代码的方式就是你大脑思考的方式,告诉脚本在需要的地方暂停。

另一个好处是你可以在不能实现的promise中使用trycatch

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的美妙之处,那么就让我们深入了解一下细节吧:

  • asyncawait是建立在Promise之上的。使用async的函数本身将始终返回一个Promise。记住这一点很重要,而且可能是你遇到的最大的“陷阱”
  • 当我们await时它会暂停函数,而不是整个代码
  • asyncawait是非阻塞的
  • 你依旧可以使用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月,几乎所有浏览器都可以使用asyncawait。更妙的是,要确保你的代码在任何地方都有效,请使用Babel来编译你的JavaScript代码,让更老的浏览器也能支持。

如果你对更多关于ES2017内容感兴趣,你可以浏览这个ES2017特性的完整列表

本文根据@ERIC WINDMILL的《Using ES2017 Async Functions》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://css-tricks.com/using-es2017-async-functions/

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.w3cplus.com/javascript/using-es2017-async-functions.html

返回顶部