使用 ES2017 异步函数

Avatar of Eric Windmill
Eric Windmill

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

ES2017 于 6 月正式发布,随之而来的是对我最喜欢的 JavaScript 特性之一 async 函数的广泛支持!如果您曾经在理解异步 JavaScript 方面遇到困难,那么这篇文章适合您。如果您没有遇到过,那么,好吧,您可能是一位超级天才。

异步函数或多或少地允许您编写顺序的 JavaScript 代码,而无需将所有逻辑都包装在回调、生成器或 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

在深入探讨 asyncawait 之前,您必须了解 Promise。为了更好地理解 Promise,我们需要再退一步,回到简单的回调函数。

Promise 在 ES6 中引入,极大地改善了在 JavaScript 中编写异步代码的方式。不再有“回调地狱”,有时人们会这样亲切地称呼它。

回调函数是可以传递给函数并在该函数内调用的函数,作为对任何事件的响应。它是 JS 的基础。

function readFile('file.txt', (data) => {
  // This is inside the callback function
  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)
            }
        }
    } 
}

这很难理解和难以跟踪。这甚至不包括其中一个文件不存在的完全可能情况下的错误处理。

我保证会更好(明白了吗?!)

这就是 Promise 可以提供帮助的地方。Promise 是一种推理尚不存在但您知道将会存在的数据的方式。Kyle Simpson 是《你不知道 JS 系列》的作者,以发表关于异步 JavaScript 的演讲而闻名。 他在这次演讲中对 Promise 的解释非常到位:就像在快餐店点餐一样。

  1. 点餐。
  2. 支付餐费并收到带有订单号的票据。
  3. 等待食物。
  4. 食物准备好了,他们会喊您的票号。
  5. 取餐。

正如他指出的那样,您可能无法在等待食物时就吃掉它,但您可以考虑它,并且可以为此做好准备。您可以继续您的一天,因为您知道食物会送达,即使您还没有收到它,因为食物已经“承诺”给您了。这就是 Promise 的全部含义。一个表示最终会存在的数据的对象。

readFile(file1)
  .then((file1-data) => { /* do something */ })
  .then((previous-promise-data) => { /* do the next thing */ })
  .catch( /* handle errors */ )

这就是 promise 语法。它的主要优点是它允许以一种直观的方式将顺序事件链接在一起。这个基本示例还可以,但是您可以看到我们仍在使用回调函数。Promise 只是回调函数的薄包装,使它更直观。

最佳方法(新):Async/Await

几年前,异步函数进入了 JavaScript 生态系统。截至上个月,它已成为该语言的正式特性,并得到广泛支持。

asyncawait 关键字是构建在 Promise 和生成器之上的薄包装。从本质上讲,它允许我们在任何我们想要的地方使用 await 关键字“暂停”我们的函数。

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

此代码运行并按预期执行。它记录了 API 调用的数据。如果您的大脑没有爆炸,我不知道如何让您满意。

这样做的好处是它很直观。您可以按照大脑思考的方式编写代码,告诉脚本在需要的地方暂停。

另一个优点是,我们可以以 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 块至少有 3 个地方可能失败,这使得它成为处理异步代码中错误的最简洁的方法。

我们还可以毫不费力地在循环和条件语句中使用异步函数

async function count() {
    let counter = 1
    for (let i = 0; i < 100; i++) {
        counter += 1
        console.log(counter)
        await sleep(1000)
    }
}

这是一个愚蠢的例子,但它将按预期运行,并且易于阅读。如果您在控制台中运行此代码,您将看到代码将在 sleep 调用上暂停,并且下一个循环迭代将在一秒钟后开始。

细节

现在您已经相信了 async 和 await 的强大功能,让我们深入了解细节。

  • 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 功能的完整列表