异步编程与异步/等待

朋友们,美好的一天!

相对而言,新增的JavaScript是异步函数和await关键字。这些功能基本上是对Promise(承诺)的语法补充,使编写和读取异步代码变得容易。它们使异步代码看起来像同步的。本文将帮助您弄清楚是什么。

条件:基本的计算机知识,JS基础知识,理解异步代码和Promise的基础。
目的:了解如何作出承诺以及如何使用它们。

异步/等待基础


使用异步/等待有两个部分。

异步关键字


首先,我们有async关键字,该关键字放在函数声明之前以使其异步。异步功能是一种预期可以使用await关键字运行异步代码的功能。

尝试在浏览器控制台中键入以下内容:

function hello(){ return 'Hello' }
hello()

该函数将返回“ Hello”。没什么特别的,对吧?

但是,如果我们将其转换为异步函数呢?请尝试以下操作:

async function hello(){ return 'Hello' }
hello()

现在,函数调用将返回一个Promise。这是异步函数的功能之一-它们返回保证被转换为Promise的值。

您还可以创建异步函数表达式,如下所示:

let hello = async function(){ return hello() }
hello()

您还可以使用箭头功能:

let hello = async () => { return 'Hello' }

所有这些功能都做同样的事情。

为了获得完成的承诺的价值,我们可以使用.then()块:

hello().then((value) => console.log(value))

...甚至是这样:

hello().then(console.log)

因此,添加async关键字会使函数返回promise而不是值。另外,它允许同步功能避免与运行和支持使用await相关的任何开销。在功能之前简单添加异步功能,即可通过JS引擎自动优化代码。凉!

等待关键字


当您将异步功能与await关键字结合使用时,异步功能的优势变得更加明显。可以在任何基于promise的函数之前添加它,以使其等待promise完成,然后返回结果。之后,将执行下一个代码块。

您可以在调用任何返回承诺的函数(包括Web API函数)时使用await。

这是一个简单的示例:

async function hello(){
    return greeting = await Promise.resolve('Hello')
}

hello().then(alert)

当然,上面的代码是没有用的,它仅用作语法的演示。让我们继续看一个真实的例子。

使用异步/等待在Promise上重写代码


以上一篇文章的获取示例为例:

fetch('coffee.jpg')
.then(response => response.blob())
.then(myBlob => {
    let objectURL = URL.createObjectURL(myBlob)
    let image = document.createElement('img')
    image.src = objectURL
    document.body.appendChild(image)
})
.catch(e => {
    console.log('There has been a problem with your fetch operation: ' + e.message)
})

您应该已经了解了什么是Promise以及它们如何工作,但是让我们使用async / await重写此代码,看看它有多简单:

async function myFetch(){
    let response = await fetch('coffee.jpg')
    let myBlob = await response.blob()

    let objectURL = URL.createObjectURL(myBlob)
    let image = document.createElement('img')
    image.src = objectURL
    document.body.appendChild(image)
}

myFetch().catch(e => {
    console.log('There has been a problem with your fetch operation: ' + e.message)
})

这使代码更加简单易懂-没有.then()块!

使用async关键字将功能转换为一个承诺,因此我们可以使用从承诺到等待的混合方法,在单独的块中突出显示该功能的第二部分,以提高灵活性:

async function myFetch(){
    let response = await fetch('coffee.jpg')
    return await response.blob()
}

myFetch().then((blob) => {
    let objectURL = URL.createObjectURL(blob)
    let image = document.createElement('image')
    image.src = objectURL
    document.body.appendChild(image)
}).catch(e => console.log(e))

您可以重写示例或运行我们的实时演示(另请参见源代码)。

但是它是如何工作的呢?


我们将代码包装在函数中,并在function关键字之前添加了async关键字。您需要创建一个异步函数来确定将在其中运行异步代码的代码块。 await仅在异步函数中起作用。

再一次:await仅在异步函数中起作用。

在myFetch()函数内部,代码非常类似于promise上的版本,但是有一些区别。不必在每个基于promise的方法之后使用.then()块,而只需在调用该方法之前添加await关键字并将一个值分配给该变量。关键字await使JS引擎在给定的行上暂停代码执行,从而允许另一个代码执行,直到异步函数返回结果为止。一旦执行,代码将从下一行继续执行。
例如:

let response = await fetch('coffee.jpg')

当给定值可用时,将fetch()承诺返回的值分配给响应变量,并且解析器将在该行停止,直到承诺完成为止。一旦该值变为可用,解析器便移至创建Blob的下一行代码。该行还调用了基于Promise的异步方法,因此在这里我们也使用await。当操作结果返回时,我们从myFetch()函数返回它。

这意味着,当我们调用myFetch()函数时,它将返回一个promise,因此我们可以在其中添加.then(),以在其中处理屏幕上图像的显示。

您可能会认为“太好了!”而且您是对的-较少用于包装代码的.then()块,它们看起来都像是同步代码,因此很直观。

添加错误处理


如果要添加错误处理,则有几种选择。

您可以将同步try ... catch结构与async / await一起使用。此示例是上面代码的扩展版本:

async function myFetch(){
    try{
        let response = await fetch('coffee.jpg')
        let myBlob = await response.blob()

        let objectURL = URL.createObjectURL(myBlob)
        let image = document.createElement('img')
        image.src = objectURL
        document.body.appendChild(image)
    } catch(e){
        console.log(e)
    }
}

myFetch()

catch(){}块接受一个错误对象,我们将其命名为“ e”;现在我们可以将其输出到控制台,这将使我们能够接收到有关代码中错误发生位置的消息。

如果要使用上面显示的代码的第二版,则应继续使用混合方法,并在.then()调用的末尾添加.catch()块,如下所示:

async function myFetch(){
    let response = await fecth('coffee.jpg')
    return await response.blob()
}

myFetch().then((blob) => {
    let objectURL = URL.createObjectURL
    let image = document.createElement('img')
    image.src = objectURL
    document.body.appendChild(image)
}).catch(e => console.log(e))

这是可能的,因为.catch()块将捕获异步函数和promise链中都发生的错误。如果在此处使用try / catch块,将无法处理调用myFetch()函数时发生的错误。

您可以在GitHub上找到两个示例:
simple-fetch-async-await-try-catch.html(请参阅源代码

simple-fetch-async-await-promise-catch.html(请参见源代码

等待Promise.all()


异步/等待基于承诺,因此您可以充分利用后者。这些特别包括Promise.all()-您可以轻松地向Promise.all()添加await以类似于同步代码的方式写入所有返回值。同样,以上一篇文章为例保持打开的标签页与下面显示的代码进行比较。

使用async / await(请参见实时演示源代码),它看起来像这样:

async function fetchAndDecode(url, type){
    let repsonse = await fetch(url)

    let content

    if(type === 'blob'){
        content = await response.blob()
    } else if(type === 'text'){
        content = await response.text()
    }

    return content
}

async function displayContent(){
    let coffee = fetchAndDecode('coffee.jpg', 'blob')
    let tea = fetchAndDecode('tea.jpg', 'blob')
    let description = fetchAndDecode('description.txt', 'text')

    let values = await Promise.all([coffee, tea, description])

    let objectURL1 = URL.createObjectURL(values[0])
    let objectURL2 = URL.createObjectURL(values[1])
    let descText = values[2]

    let image1 = document.createElement('img')
    let image2 = document.createElement('img')
    image1.src = objectURL1
    image2.src = objectURL2
    document.body.appendChild(image1)
    document.body.appendChild(image2)

    let para = document.createElement('p')
    para.textContent = descText
    document.body.appendChild(para)
}

displayContent()
.catch(e => console.log(e))

我们很容易通过一些更改使fetchAndDecode()函数异步。注意这一行:

let values = await Promise.all([coffee, tea, description])

使用await,我们以类似于同步代码的方式获得变量值中三个promise的结果。我们必须将整个函数包装在一个新的异步函数displayContent()中。我们没有实现强大的代码缩减,但是能够从.then()块中提取大多数代码,这提供了有用的简化,并使代码更具可读性。

为了处理错误,我们在对displayContent()的调用中添加了.catch()块;它处理两个函数的错误。

请记住:您还可以使用.finally()块获取有关该操作的报告-您可以在我们的实时演示中看到它的运行情况(另请参见源代码)。

异步/等待劣势


异步/等待有两个缺陷。

异步/等待使代码看起来像同步的,从某种意义上讲,使其行为更加同步。与同步操作中发生的情况一样,关键字await阻止其后的代码执行,直到承诺完成为止。这允许您执行其他任务,但是您自己的代码已锁定。

这意味着您的代码可能会因大量的未决承诺而被放慢速度。每次等待都将等待上一个的完成,而我们希望同时实现承诺,就好像我们没有使用异步/等待一样。

有一种设计模式可以缓解此问题-通过将Promise对象存储在变量中然后等待它们来禁用所有promise过程。让我们看看这是如何实现的。

我们有两个示例可供使用slow-async-await.html(请参阅源代码)和fast-async-await.html(请参见源代码)。这两个示例均以promise函数开头,该函数使用setTimeout()模仿异步操作:

function timeoutPromise(interval){
    return new Promise((resolve, reject) => {
        setTimeout(function(){
            resolve('done')
        }, interval)
    })
}

然后是异步函数timeTest(),该函数希望对timeoutPromise()进行三个调用:

async function timeTest(){
    ...
}

对timeTest()的三个调用中的每一个都以记录完成承诺的时间结束,然后记录完成整个操作所花费的时间:

let startTime = Date.now()
timeTest().then(() => {
    let finishTime = Date.now()
    let timeTaken = finishTime - startTime
    alert('Time taken in milliseconds: ' + timeTaken)
})

在每种情况下,timeTest()函数都不同。

在slow-async-await.html中,timeTest()如下所示:

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

在这里,我们只希望对timeoutPromise进行三个调用,每次设置3秒的延迟。每个调用都等待上一个调用的完成-如果运行第一个示例,则将在9秒钟左右看到一个模态窗口。

在fast-async-await.html中,timeTest()如下所示:

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

    await timeoutPromise1
    await timeoutPromise2
    await timeoutPromise3
}

在这里,我们将三个Promise对象保存在变量中,这将导致与其关联的进程同时运行。

此外,我们期望他们的结果-当诺言同时开始履行时,诺言也将同时完成;当您运行第二个示例时,您将在3秒钟左右看到一个模态窗口!

您应该仔细测试代码,并在降低性能的同时牢记这一点。

另一个不便之处是需要将预期的承诺包装在异步函数中。

对类使用异步/等待


总之,我们注意到您甚至可以在用于创建类的方法中添加异步,以便它们返回promise并在其中等待promise。有关面向对象JS文章中获取代码,并将其与使用async修改的版本进行比较:

class Person{
    constructor(first, last, age, gender, interests){
        this.name = {
            first,
            last
        }
        this.age = age
        this.gender = gender
        this.interests = interests
    }

    async greeting(){
        return await Promise.resolve(`Hi! I'm ${this.name.first}`)
    }

    farewell(){
        console.log(`${this.name.first} has left the building. Bye for now!`)
    }
}

let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling'])

该类方法可以如下使用:

han.greeting().then(console.log)

浏览器支持


使用异步/等待的障碍之一是缺乏对较旧浏览器的支持。几乎所有现代浏览器都可以使用此功能。Internet Explorer和Opera Mini中存在一些问题。

如果要使用async / await,但需要较旧浏览器的支持,则可以使用BabelJS-它允许您使用最新的JS,并将其转换为适合特定浏览器的JS。

结论


异步/等待允许您编写易于阅读和维护的异步代码。尽管与其他异步代码编写方法相比,对async / await的支持更差,但是绝对值得探索。

感谢您的关注。

编码愉快!

All Articles