Précédent: Présentation du CMS Apostrophe (3/3)

Suivant: SSL : Let's encrypt + Docker

ES7: async/await dans une boucle

await ne peut être utilisé qu'à l'intérieur d'une fonction async. C'est un problème lorsqu'on a besoin d'une des fonctions du prototype Array en JS telles que map ou forEach qui utilisent des callbacks.

Array.prototype.map

Dans le code suivant, des urls dans un array sont téléchargées et renvoyées dans un autre array.

async function downloadContent (urls) {
    return urls.map(url => {
        // Mauvaise syntaxe
        const content = await httpGet(url);
        return content;
    });
}

Cela ne fonctionne pas, car await a besoin d'être dans une fonction asynchrone async. On pourrait penser qu'il suffit d'ajouter async dans la boucle :

async function downloadContent (urls) {
    return urls.map(async (url) => {
        const content = await httpGet(url);
        return content;
    });
}

Il y a deux problèmes avec ce code:

Le résultat est maintenant un tableau de promesses, pas un tableau de string.
Les callbacks ne sont pas achevées une fois que map() est terminé, car await n'interrompt que sa fonction parente et que httpGet() est résolu de manière asynchrone.

Nous pouvons résoudre les deux problèmes via Promise.all, qui convertit un tableau de promesses en une sorte de promesse parente :

async function downloadContent (urls) {
    const promiseArray = urls.map(url => httpGet (url));
    return Promise.all(promiseArray);
}

Array.prototype.forEach

Avec forEach(), voici comment traiter le même exemple :

async function logContent (urls) {
    urls.forEach(url => {
        // Mauvaise syntaxe
        const content = await httpGet (url);
        console.log(content);
    });
}

Encore une fois, ce code produira une erreur de syntaxe, car on ne peut pas utiliser await dans les fonctions synchrones.

Avec une fonction asynchrone:

async function logContent (urls) {
    urls.forEach(async url => {
        const content = await httpGet(url);
        console.log(content);
    });
    // Promesses pas encore résolues ici
}

Cela fonctionne techniquement, mais il faut savoir une chose : la promesse retournée par httpGet est résolue de manière asynchrone, ce qui signifie que les callbacks ne sont pas terminés lorsque forEach se termine. Par conséquent, logContent ne pourras pas être await par une autre fonction si besoin.

Dans ce cas, une boucle for-of est nécessaire :

async function logContent (urls) {
    for (const url of urls) {
        const content = await httpGet(url);
        console.log(content);
    }
}

Désormais, les promesses de logContent sont résolues à la sortie de la boucle for-of. Cependant, les étapes de traitement se déroulent de manière séquentielle : le 2e appel à httpGet n'est appelé qu'après la fin du premier appel du 1er passage dans la boucle.

En cas de besoin de parallélisation, il faut utiliser Promise.all :

async function logContent (urls) {
    await Promise.all(urls.map(
        async url => {
            const content = await httpGet(url);
            console.log(content);
        }));
}

map est utilisé ici pour créer un tableau de promesses qui se remplit au fur et à mesure. Les promesses seront résolues à la fin de la fonction.

Conclusion

Pour résumer, la méthode forEach lance les promesses sans attendre leur résolution, elle est donc fortement déconseillée. Il vaut mieux utiliser une boucle for-of pour un déroulé séquentiel de plusieurs promesses. Et Promise.all avec map pour des résolutions en parallèle.

Précédent: Présentation du CMS Apostrophe (3/3)

Suivant: SSL : Let's encrypt + Docker