Programmation asynchrone avec async / wait

Bonjour mes amis!

Les ajouts JavaScript relativement nouveaux sont les fonctions asynchrones et le mot-clé wait. Ces fonctionnalités sont essentiellement du sucre syntaxique au-dessus des promesses (promesses), ce qui facilite l'écriture et la lecture de code asynchrone. Ils font que le code asynchrone ressemble à synchrone. Cet article vous aidera à comprendre ce qui est quoi.

Conditions: connaissances informatiques de base, connaissance des bases de JS, compréhension des bases du code asynchrone et promesses.
Objectif: Comprendre comment les promesses sont faites et comment elles sont utilisées.

Async / attente des bases


L'utilisation de async / wait comporte deux parties.

Mot-clé asynchrone


Tout d'abord, nous avons le mot-clé async, que nous mettons avant la déclaration de fonction pour le rendre asynchrone. Une fonction asynchrone est une fonction qui anticipe la possibilité d'utiliser le mot clé wait pour exécuter du code asynchrone.

Essayez de taper ce qui suit dans la console du navigateur:

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

La fonction renverra «Bonjour». Rien d'inhabituel, non?

Mais que faire si nous la transformons en fonction asynchrone? Essayez ce qui suit:

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

Maintenant, un appel de fonction renvoie une promesse. C'est l'une des caractéristiques des fonctions asynchrones - elles renvoient des valeurs qui sont garanties d'être converties en promesses.

Vous pouvez également créer des expressions fonctionnelles asynchrones, comme ceci:

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

Vous pouvez également utiliser les fonctions fléchées:

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

Toutes ces fonctions font la même chose.

Afin d'obtenir la valeur d'une promesse terminée, nous pouvons utiliser le bloc .then ():

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

... ou même ainsi:

hello().then(console.log)

Ainsi, l'ajout du mot clé async fait que la fonction retourne une promesse au lieu d'une valeur. De plus, il permet aux fonctions synchrones d'éviter toute surcharge associée à l'exécution et à la prise en charge de l'attente. Un simple ajout d'async devant la fonction permet une optimisation automatique du code par le moteur JS. Cool!

Mot clé en attente


Les avantages des fonctions asynchrones deviennent encore plus apparents lorsque vous les combinez avec le mot-clé wait. Il peut être ajouté avant toute fonction basée sur une promesse pour faire attendre la fin de la promesse, puis renvoyer le résultat. Après cela, le bloc de code suivant est exécuté.

Vous pouvez utiliser wait lorsque vous appelez une fonction qui renvoie une promesse, y compris les fonctions de l'API Web.

Voici un exemple trivial:

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

hello().then(alert)

Bien sûr, le code ci-dessus est inutile, il ne sert que de démonstration de la syntaxe. Passons à autre chose et regardons un exemple réel.

Réécriture du code sur les promesses en utilisant async / wait


Prenez l'exemple de récupération de l'article précédent:

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

Vous devriez déjà comprendre ce que sont les promesses et comment elles fonctionnent, mais réécrivons ce code en utilisant async / wait pour voir à quel point c'est simple:

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

Cela rend le code beaucoup plus simple et plus facile à comprendre - pas de blocs .then ()!

L'utilisation du mot-clé async transforme une fonction en promesse, nous pouvons donc utiliser une approche mixte à partir des promesses et attendre, en mettant en évidence la deuxième partie de la fonction dans un bloc séparé afin d'augmenter la flexibilité:

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

Vous pouvez réécrire l'exemple ou exécuter notre démo en direct (voir aussi le code source ).

Mais comment ça fonctionne?


Nous avons encapsulé le code à l'intérieur de la fonction et ajouté le mot clé async avant le mot clé function. Vous devez créer une fonction asynchrone pour déterminer le bloc de code dans lequel le code asynchrone s'exécutera; attendre ne fonctionne que dans les fonctions asynchrones.

Encore une fois: attendre ne fonctionne que dans les fonctions asynchrones.

À l'intérieur de la fonction myFetch (), le code ressemble beaucoup à la version promise, mais avec quelques différences. Au lieu d'utiliser le bloc .then () après chaque méthode basée sur la promesse, ajoutez simplement le mot clé wait avant d'appeler la méthode et attribuez une valeur à la variable. Le mot-clé wait fait interrompre l'exécution du code JS sur une ligne donnée, permettant à un autre code de s'exécuter jusqu'à ce que la fonction asynchrone renvoie un résultat. Une fois exécuté, le code continuera son exécution à partir de la ligne suivante.
Par exemple:

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

La valeur renvoyée par la promesse fetch () est affectée à la variable de réponse lorsque la valeur donnée devient disponible, et l'analyseur s'arrête à cette ligne jusqu'à ce que la promesse soit terminée. Dès que la valeur devient disponible, l'analyseur passe à la ligne de code suivante qui crée le blob. Cette ligne appelle également la méthode asynchrone basée sur les promesses, donc ici nous utilisons également wait. Lorsque le résultat de l'opération revient, nous le renvoyons à partir de la fonction myFetch ().

Cela signifie que lorsque nous appelons la fonction myFetch (), elle renvoie une promesse, nous pouvons donc y ajouter .then (), à l'intérieur de laquelle nous gérons l'affichage de l'image à l'écran.

Vous pensez probablement "C'est génial!" Et vous avez raison - moins de blocs .then () pour encapsuler le code, tout cela ressemble à du code synchrone, donc c'est intuitif.

Ajouter la gestion des erreurs


Si vous souhaitez ajouter la gestion des erreurs, vous avez plusieurs options.

Vous pouvez utiliser la structure synchrone try ... catch avec async / wait. Cet exemple est une version étendue du code ci-dessus:

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

Le bloc catch () {} accepte un objet d'erreur, que nous avons nommé "e"; Maintenant, nous pouvons le sortir sur la console, cela nous permettra de recevoir un message indiquant où l'erreur s'est produite dans le code.

Si vous souhaitez utiliser la deuxième version du code ci-dessus, vous devez simplement continuer d'utiliser l'approche hybride et ajouter le bloc .catch () à la fin de l'appel .then (), comme suit:

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

Cela est possible car le bloc .catch () interceptera les erreurs qui se produisent à la fois dans la fonction asynchrone et dans la chaîne de promesses. Si vous utilisez le bloc try / catch ici, vous ne pourrez pas gérer les erreurs qui se produisent lorsque la fonction myFetch () est appelée.

Vous pouvez trouver les deux exemples sur GitHub:
simple-fetch-async-wait-try-catch.html (voir source )

simple-fetch-async-wait-promise-catch.html (voir source )

En attente de Promise.all ()


Async / Wait est basé sur des promesses, vous pouvez donc profiter pleinement de ces dernières. Il s'agit notamment de Promise.all () en particulier - vous pouvez facilement ajouter attendre à Promise.all () pour écrire toutes les valeurs de retour d'une manière similaire au code synchrone. Encore une fois, reprenons l' exemple de l'article précédent . Gardez un onglet ouvert avec lui pour comparer avec le code ci-dessous.

Avec async / wait (voir la démo en direct et le code source ), cela ressemble à ceci:

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

Nous avons facilement rendu la fonction fetchAndDecode () asynchrone avec quelques modifications. Faites attention à la ligne:

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

En utilisant wait, nous obtenons les résultats de trois promesses dans la variable values, d'une manière similaire au code synchrone. Nous devons encapsuler la fonction entière dans une nouvelle fonction asynchrone, displayContent (). Nous n'avons pas obtenu une forte réduction de code, mais avons pu extraire la majeure partie du code du bloc .then (), ce qui fournit une simplification utile et rend le code plus lisible.

Pour gérer les erreurs, nous avons ajouté un bloc .catch () à notre appel à displayContent (); Il gère les erreurs des deux fonctions.

N'oubliez pas: vous pouvez également utiliser le bloc .finally () pour obtenir un rapport sur l'opération - vous pouvez le voir en action dans notre démo en direct (voir aussi le code source ).

Asynchroniser / attendre les inconvénients


Async / attendent a quelques défauts.

Async / Wait donne au code un aspect synchrone et, dans un sens, le fait se comporter de manière plus synchrone. Le mot-clé wait bloque l'exécution du code qui le suit jusqu'à la fin de la promesse, comme cela se produit dans une opération synchrone. Cela vous permet d'effectuer d'autres tâches, mais votre propre code est verrouillé.

Cela signifie que votre code peut être ralenti par un grand nombre de promesses en attente qui se succèdent. Chaque attente attendra la fin de la précédente, tandis que nous aimerions que les promesses soient tenues simultanément, comme si nous n'utilisions pas async / wait.

Il existe un modèle de conception pour atténuer ce problème - la désactivation de tous les processus de promesse en stockant les objets Promise dans des variables, puis en les attendant. Voyons comment cela est mis en œuvre.

Nous avons deux exemples à notre disposition: slow-async-wait.html (voir le code source ) et fast-async-wait.html (voir le code source ). Les deux exemples commencent par une fonction de promesse qui imite une opération asynchrone à l'aide de setTimeout ():

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

Suit ensuite la fonction asynchrone timeTest (), qui attend trois appels à timeoutPromise ():

async function timeTest(){
    ...
}

Chacun des trois appels à timeTest () se termine par un enregistrement du temps qu'il a fallu pour remplir la promesse, puis le temps qu'il faut pour terminer l'ensemble de l'opération est enregistré:

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

Dans chaque cas, la fonction timeTest () est différente.

Dans slow-async-wait.html timeTest () ressemble à ceci:

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

Ici, nous attendons seulement trois appels à timeoutPromise, en définissant à chaque fois un délai de 3 secondes. Chaque appel attend la fin du précédent - si vous exécutez le premier exemple, vous verrez une fenêtre modale dans environ 9 secondes.

Dans fast-async-wait.html timeTest () ressemble à ceci:

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

    await timeoutPromise1
    await timeoutPromise2
    await timeoutPromise3
}

Ici, nous enregistrons trois objets Promise dans des variables, ce qui entraîne l'exécution simultanée des processus qui lui sont associés.

De plus, nous attendons leurs résultats - comme les promesses commencent à être tenues simultanément, les promesses seront également réalisées en même temps; lorsque vous exécutez le deuxième exemple, vous verrez une fenêtre modale dans environ 3 secondes!

Vous devez soigneusement tester le code et garder cela à l'esprit tout en réduisant les performances.

Un autre inconvénient mineur est la nécessité d'encapsuler les promesses attendues dans une fonction asynchrone.

Utiliser async / wait avec les classes


En conclusion, nous notons que vous pouvez ajouter async même dans les méthodes de création de classes afin qu'elles retournent des promesses et attendent des promesses à l'intérieur. Prenez le code de l' article sur JS orienté objet et comparez-le avec la version modifiée en utilisant 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'])

La méthode de classe peut être utilisée comme suit:

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

Prise en charge du navigateur


L'un des obstacles à l'utilisation d'Async / Wait est le manque de prise en charge des anciens navigateurs. Cette fonctionnalité est disponible dans presque tous les navigateurs modernes, ainsi que dans les promesses; Certains problèmes existent dans Internet Explorer et Opera Mini.

Si vous souhaitez utiliser async / wait, mais avez besoin de la prise en charge d'anciens navigateurs, vous pouvez utiliser la bibliothèque BabelJS - elle vous permet d'utiliser la dernière version de JS, en la convertissant en une version spécifique au navigateur.

Conclusion


Async / Wait vous permet d'écrire du code asynchrone facile à lire et à gérer. Bien que asynchrone / attente soit pire pris en charge que d'autres façons d'écrire du code asynchrone, cela vaut vraiment la peine d'être exploré.

Merci de votre attention.

Bon codage!

All Articles