Promesses JavaScript: introduction

Les promesses simplifient les calculs différés et asynchrones. Une promesse représente une opération qui n'est pas encore terminée.

Jake Archibald
Jake Archibald

Développeurs, préparez-vous à un moment charnière de l'histoire développement Web.

[Début des roulements de tambour]

Les promesses sont arrivées en JavaScript !

[Un feu d'artifice explose, une pluie de papier scintillant s'écoule d'en haut, la foule se déchaîne]

À ce stade, vous entrez dans l'une de ces catégories:

  • Les gens vous encouragent, mais vous n'êtes pas sûr de comprendre à propos. Peut-être que vous n'êtes même pas sûr de ce qu'est une "promesse" l'adresse IP interne. Vous augmenteriez les épaules, mais le poids du papier scintillant pèse sur vos épaules. Si oui, évitez il m'a fallu du temps pour comprendre pourquoi je devais m'en soucier, des trucs. Vous voudrez probablement commencer par le début.
  • Quel talent ! Il était temps, n'est-ce pas ? Vous avez déjà utilisé ces promesses mais cela vous dérange lorsque toutes les implémentations ont une API légèrement différente. Quelle est l'API pour la version JavaScript officielle ? Vous voudrez probablement commencer avec la terminologie.
  • Vous le saviez déjà et vous vous moquez de ceux qui sautent et comme si c’était une nouvelle pour eux. Prenez un moment pour profiter de votre propre supériorité, accédez directement à la documentation de référence de l'API.

Prise en charge des navigateurs et polyfill

Navigateurs pris en charge

  • Chrome: 32 <ph type="x-smartling-placeholder">
  • Edge: 12 <ph type="x-smartling-placeholder">
  • Firefox: 29 <ph type="x-smartling-placeholder">
  • Safari: 8. <ph type="x-smartling-placeholder">

Source

Mettre à jour les navigateurs dont l'implémentation des promesses n'est pas complète ou ajouter des promesses à d'autres navigateurs et à Node.js, consultez le polyfill (2 000 fichiers compressés avec gzip).

De quoi s'agit-il ?

JavaScript est monothread, ce qui signifie que deux bits de script ne peuvent pas s'exécuter en même temps ; elles doivent s'exécuter l'une après l'autre. Dans les navigateurs, JavaScript partage un fil de discussion avec un grand nombre d'éléments différents d'un navigateur à l'autre navigateur. Mais JavaScript se trouve généralement dans la même file d'attente que le code et la gestion des actions des utilisateurs (mise en surbrillance du texte, avec les commandes de formulaire). Toute activité liée à l'un de ces éléments retarde les autres.

En tant qu'être humain, vous êtes multithread. Vous pouvez saisir du texte avec plusieurs doigts, vous pouvez conduire et tenir une conversation en même temps. Le seul blocage que nous devons traiter, c'est qu'il éternue, alors que toute activité en cours doit être suspendue pendant la durée de l'éternuement. C'est assez agaçant, surtout lorsque vous conduisez et essayez de tenir une conversation. Vous ne devez pas vous voulez écrire du code facile à utiliser.

Vous avez probablement utilisé des événements et des rappels pour contourner ce problème. Voici les événements:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

Ce n'est pas du tout éternel. Nous obtenons l'image, ajoutons quelques écouteurs, JavaScript peut arrêter de s'exécuter jusqu'à ce que l'un de ces écouteurs soit appelé.

Malheureusement, dans l'exemple ci-dessus, il est possible que les événements avant de commencer à les écouter. Nous devons donc contourner ce problème en utilisant l'instruction "complete" des images:

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

Cela ne détecte pas les images erronées avant que nous ayons pu les écouter them; malheureusement, le DOM ne nous permet pas de le faire. De plus, il s'agit charger une image. Les choses deviennent encore plus complexes si nous voulons savoir quand un ensemble d'images ont été chargées.

Les événements ne sont pas toujours le meilleur moyen

Les événements sont parfaits pour les choses qui peuvent se produire plusieurs fois au cours d'une même objet : keyup, touchstart, etc. Avec ces événements, vous ne vous souciez pas vraiment ce qui s'est passé avant d'associer l'écouteur. Mais quand il s’agit de en cas de réussite ou d'échec asynchrone. Idéalement, vous devez obtenir un résultat semblable à celui-ci:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

C'est ce que font les promesses, mais avec des noms mieux adaptés. Si les éléments d'image HTML comportaient un "prêt" qui renvoyait une promesse, nous pouvons procéder comme suit:

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

Dans leur forme la plus élémentaire, les promesses ressemblent un peu aux écouteurs d'événements, à l'exception des éléments suivants:

  • Une promesse ne peut réussir ou échouer qu'une seule fois. Elle ne peut pas réussir ni échouer deux fois, ni de la réussite à l'échec, et inversement.
  • Si une promesse a réussi ou échoué et que vous ajoutez ultérieurement une erreur le rappel correct sera appelé, même si l'événement plus tôt.

Ceci est extrêmement utile en cas de réussite ou d'échec asynchrone, car vous s'intéresse à la date exacte de sa mise à disposition, et s'intéresse davantage à réagir au résultat.

Terminologie liée aux promesses

La première ébauche de Domenic Denicola a été lue de cet article et j'ai noté la note "F" pour connaître la terminologie. Il m'a mis en détention, m'a forcé à copier États et destins 100 fois et j'ai écrit une lettre inquiète à mes parents. Malgré cela, je continue une grande partie de la terminologie confondue, mais voici les bases:

Une promesse peut être:

  • fulfillment : l'action relative à la promesse a été effectuée.
  • rejected : l'action relative à la promesse a échoué.
  • pending (en attente) : n'a pas encore été traité ou refusé.
  • réglé : a été traité ou refusé.

Spécifications utilise également le terme thenable pour décrire un objet semblable à une promesse, car il comporte une méthode then. Ce terme me rappelle l'histoire du football anglais Responsable de Terry Venables, Je vais l'utiliser le moins possible.

Les promesses arrivent en JavaScript !

Des promesses existent depuis un certain temps sous la forme de bibliothèques, telles que:

Les promesses ci-dessus et JavaScript partagent un comportement commun et standardisé appelé Promes/A+ (Promesses/A+). Si si vous utilisez jQuery, ils ont un service similaire, appelé Différé : Toutefois, Les reports ne sont pas conformes aux promesses/A+, ce qui les rend subtilement différent et moins utile, alors faites attention. jQuery dispose également du type de promesse, mais il ne s'agit sous-ensemble de Deferred et rencontre les mêmes problèmes.

Bien que les implémentations de promesses suivent un comportement standardisé, leurs diffèrent globalement. Les promesses JavaScript sont semblables dans l'API à Répondre.js. Voici comment créer une promesse:

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

Le constructeur de promesse utilise un argument, un rappel avec deux paramètres, résoudre et rejeter. Effectuez une action dans le rappel (éventuellement asynchrone), puis appelez résoudre si tout a fonctionné, dans le cas contraire, appeler rejetter.

Comme throw dans le code JavaScript simple, il est courant, mais pas obligatoire, de rejettent avec un objet Error. L'avantage des objets Error est qu'ils capturent trace de la pile, ce qui rend les outils de débogage plus utiles.

Voici comment vous pouvez tenir cette promesse:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then() accepte deux arguments, un rappel pour les cas de réussite, et un autre en cas d'échec. Les deux sont facultatifs. Vous pouvez donc ajouter un rappel pour en cas de réussite ou d'échec uniquement.

Les promesses JavaScript ont débuté dans le DOM sous le nom "Futures", et ont été renommées "Promes" avant de passer à JavaScript. Les avoir en JavaScript au lieu de Le DOM est idéal, car ils sont disponibles dans des contextes JavaScript autres que les navigateurs, Node.js (s'ils les utilisent dans leurs API de base est une autre question).

Bien qu'il s'agisse d'une fonctionnalité JavaScript, le DOM n'a pas peur de les utiliser. Dans En fait, toutes les nouvelles API DOM avec des méthodes asynchrones de réussite ou d'échec utiliseront des promesses. Cela se produit déjà avec Gestion des quotas, Événements de chargement de police ServiceWorker, Web MIDI, Flux et plus encore.

Compatibilité avec d'autres bibliothèques

JavaScript promet que l'API traitera tout élément comportant une méthode then() comme comme promesse (ou thenable en soupir promis), donc si vous utilisez une bibliothèque qui renvoie une promesse Q. JavaScript promet.

Cependant, comme je vous l'ai dit, les fichiers différés de jQuery ne sont pas très utiles. Heureusement, vous pouvez les formuler selon des promesses standards, ce qui en vaut la peine. dès que possible:

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

Ici, le champ $.ajax de jQuery renvoie une valeur Deferred. Comme il s'agit d'une méthode then(), Promise.resolve() peut la transformer en promesse JavaScript. Toutefois, Les fichiers différés transmettent parfois plusieurs arguments à leurs rappels, par exemple:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

Alors que JS promet d'ignorer tous les éléments, sauf le premier:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

Heureusement, c'est généralement ce que vous voulez, ou du moins vous donne accès à ce que vous voulez. Notez également que jQuery ne respecte pas la convention la transmission d'objets Error dans les refus.

Code asynchrone complexe simplifié

Passons à la programmation. Supposons que nous voulions:

  1. Lancer une icône de chargement pour indiquer le chargement
  2. Récupérez du code JSON pour une histoire, ce qui nous donne le titre et les URL de chaque chapitre.
  3. Ajouter un titre à la page
  4. Explorez chaque chapitre
  5. Ajouter l'histoire à la page
  6. Arrêter l'icône de chargement

... mais aussi indiquer à l'utilisateur si quelque chose s'est mal passé en cours de route. Nous voudrions pour arrêter la roue à cet endroit, sinon elle continue de tourner. le vertige et de planter dans une autre UI.

Bien sûr, vous n'utiliseriez pas JavaScript pour livrer une histoire, la diffusion au format HTML est plus rapide, mais ce modèle est assez courant avec les API: plusieurs types de données puis de faire quelque chose une fois que tout est fait.

Pour commencer, voyons comment récupérer des données à partir du réseau:

La requête XMLHttpRequest est prometteuse

Les anciennes API seront mises à jour pour utiliser les promesses, si c'est possible dans une version antérieure compatible. XMLHttpRequest est un candidat de choix, mais en attendant, écrivons une fonction simple pour effectuer une requête GET:

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

Utilisons-le maintenant:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

Nous pouvons maintenant effectuer des requêtes HTTP sans saisir manuellement XMLHttpRequest, ce qui est très bien, moins je dois voir les casse-tête de chameau de XMLHttpRequest, plus ma vie sera heureuse.

Chaîne

then() n'est pas la fin de l'histoire. Vous pouvez enchaîner des then pour transformer les valeurs ou exécuter des actions asynchrones supplémentaires l'une après l'autre.

Transformer des valeurs

Vous pouvez transformer les valeurs simplement en renvoyant la nouvelle valeur:

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

À titre d'exemple pratique, revenons à:

get('story.json').then(function(response) {
  console.log("Success!", response);
})

La réponse est au format JSON, mais nous la recevons actuellement en texte brut. Mer nous pourrions modifier la fonction GET pour qu'elle utilise responseType, mais nous pourrions aussi le résoudre dans les promesses:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

Comme JSON.parse() accepte un seul argument et renvoie une valeur transformée, nous pouvons créer un raccourci:

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

En fait, nous pourrions même créer très facilement une fonction getJSON():

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON() renvoie toujours une promesse qui récupère une URL, puis l'analyse la réponse au format JSON.

Mettre des actions asynchrones en file d'attente

Vous pouvez également enchaîner des then pour exécuter des actions asynchrones dans l'ordre.

Lorsque vous renvoyez un élément à partir d'un rappel then(), c'est un peu magique. Si vous renvoyez une valeur, l'élément then() suivant est appelé avec cette valeur. Toutefois, si vous renvoyez un résultat semblable à une promesse, le then() suivant attend dessus et est n'est appelé que lorsque la promesse est établie (réussite/échec). Exemple :

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

Ici, nous envoyons une requête asynchrone à story.json, ce qui nous donne un ensemble de URL à demander, nous demandons la première d'entre elles. C'est là que les promesses à se démarquer des modèles de rappel simples.

Vous pouvez même créer un raccourci pour accéder aux chapitres:

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

Nous ne téléchargeons pas story.json tant que getChapter n'est pas appelé, mais la prochaine Heure(s) d'appel de getChapter, nous réutilisons la promesse de l'histoire. story.json n'est récupérée qu'une seule fois. Waouh, promesses !

Gestion des exceptions

Comme nous l'avons vu précédemment, then() accepte deux arguments : un pour la réussite, un en cas d'échec (ou d'honorer et de rejeter, comme promis):

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

Vous pouvez également utiliser catch():

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

catch() n'a rien de spécial, c'est juste du sucre pour then(undefined, func), mais il est plus lisible. Notez que les deux codes exemples ci-dessus ne se comportent pas de la même façon, ce dernier est équivalent à:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

La différence est subtile, mais extrêmement utile. Ignorer les refus de promesse au then() suivant avec un rappel de refus (ou catch(), puisque son équivalent). Avec then(func1, func2), func1 ou func2 sera mais jamais les deux. Mais avec then(func1).catch(func2), les deux seront appelé en cas de refus de func1, car il s'agit d'étapes distinctes de la chaîne. Prendre les éléments suivants:

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

Le flux ci-dessus est très semblable à la méthode "try/catch" en JavaScript normale : les erreurs se produisent au cours d'une phase accédez immédiatement au bloc catch(). Voici les ci-dessus sous forme d'organigramme (parce que j'adore les organigrammes):

Suivez les lignes bleues pour les promesses qui sont respectées ou les lignes rouges pour celles qui sont respectées. refuser.

Exceptions et promesses JavaScript

Le refus se produit lorsqu'une promesse est explicitement refusée, mais aussi implicitement Si une erreur est générée dans le rappel du constructeur:

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

Cela signifie qu'il est utile d'effectuer tout le travail lié à la promesse dans le le rappel du constructeur de promesse, de sorte que les erreurs sont automatiquement interceptées et deviennent des refus.

Il en va de même pour les erreurs générées dans les rappels then().

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

Traitement des erreurs en pratique

Avec notre histoire et nos chapitres, nous pouvons utiliser catch pour présenter une erreur à l'utilisateur:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

Si la récupération de story.chapterUrls[0] échoue (par exemple, HTTP 500 ou l'utilisateur est hors connexion), il ignorera tous les rappels de réussite suivants, y compris celui dans getJSON(), qui tente d'analyser la réponse au format JSON et ignore également l'action qui ajoute chapitre1.html à la page. Au lieu de cela, il passe sur le loquet . Par conséquent, le message "Échec de l'affichage du chapitre" est ajouté à la page si l'une des actions précédentes ont échoué.

Tout comme la méthode try/catch de JavaScript, l'erreur est interceptée et le code suivant est repéré l'icône de chargement est toujours masquée, ce que nous voulons. La ci-dessus devient une version asynchrone non bloquante de:

try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
}
catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'

Vous voudrez peut-être catch() simplement à des fins de journalisation, sans récupérer de l'erreur. Pour ce faire, renvoyez simplement l'erreur. Nous pourrions le faire dans notre méthode getJSON():

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

Nous avons réussi à extraire un chapitre, mais nous voulons tous les avoir. Faisons qui se produisent.

Parallélisme et séquencement: tirer le meilleur parti des deux

Penser asynchrone n'est pas facile. Si vous avez du mal à vous démarquer, essayez d'écrire le code comme s'il était synchrone. Dans ce cas :

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'

Ça marche ! Mais il s'agit d'une synchronisation qui verrouille le navigateur pendant le téléchargement. À rendre ce travail asynchrone. Nous utilisons then() pour que les choses se produisent l'une après l'autre.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: for each url in story.chapterUrls, fetch &amp; display
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

Mais comment faire une boucle dans les URL des chapitres et les récupérer dans l'ordre ? Ce ne fonctionne pas:

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach n'étant pas en mode asynchrone, les chapitres peuvent apparaître dans n'importe quel ordre. qu'ils téléchargent. C'est ainsi qu'a été écrit Pulp Fiction. Ce n'est pas Pulp Fiction, alors résolvons le problème.

Créer une séquence

Nous voulons transformer notre tableau chapterUrls en une séquence de promesses. Pour ce faire, nous pouvons utiliser then():

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

C'est la première fois que nous voyons Promise.resolve(), qui crée une prometteuse qui répond à la valeur que vous lui donnez. Si vous lui transmettez une instance de Promise, la fonction la renverra simplement (remarque:il s'agit d'un modification des spécifications que certaines implémentations ne respectent pas encore). Si vous transmettez-lui quelque chose de semblable à une promesse (dispose d'une méthode then()), il crée un Promise authentique qui répond ou refuse de la même manière. Si vous réussissez dans toute autre valeur, comme Promise.resolve('Hello'), il crée un prometteuse qui offre cette valeur. Si vous l'appelez sans valeur, comme ci-dessus, elle répond avec la valeur "undefined".

Il existe également Promise.reject(val), qui crée une promesse qui rejette avec la valeur que vous lui attribuez (ou non définie).

Nous pouvons nettoyer le code ci-dessus en utilisant array.reduce:

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

Le processus est le même que dans l'exemple précédent, mais il n'est pas nécessaire "séquence" . Notre rappel de réduction est appelé pour chaque élément du tableau. "séquence" correspond à Promise.resolve() la première fois, mais pour le reste appelle "Sequence" correspond au résultat de l'appel précédent. array.reduce est très utile pour réduire un tableau à une valeur unique, ce qui, dans notre cas, est une promesse.

Réunissons le tout:

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

Et voilà, nous avons une version entièrement asynchrone de la version de synchronisation. Mais nous pouvons faire mieux encore. Pour le moment, notre page est en cours de téléchargement comme ceci:

Les navigateurs sont assez efficaces pour télécharger plusieurs éléments à la fois, donc nous perdons en téléchargeant les chapitres l'un après l'autre. Ce que nous voulons faire, les télécharger tous en même temps, puis les traiter une fois qu'ils sont arrivés. Heureusement, il existe une API pour cela:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all prend un ensemble de promesses et crée une promesse qui répond à leurs attentes quand elles ont toutes terminé avec succès. Vous obtenez un tableau de résultats dans le même ordre que les promesses que vous avez transmises.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

En fonction de la connexion, cela peut être quelques secondes plus rapide que le chargement un par un, et nécessitent moins de code que la première fois. Vous pouvez télécharger les chapitres mais elles apparaissent à l'écran dans le bon ordre.

Toutefois, nous pouvons tout de même améliorer les performances perçues. Quand le premier chapitre arrive, doit l'ajouter à la page. Cela permet à l'utilisateur de commencer à lire avant le reste les chapitres sont arrivés. À l'arrivée du chapitre 3, nous ne l'ajouterions plus car l'utilisateur peut ne pas se rendre compte qu'il manque le chapitre 2. Quand le chapitre deux nous pouvons ajouter les chapitres deux et trois, etc.

Pour ce faire, nous récupérons le JSON de tous nos chapitres en même temps, puis nous créons une pour les ajouter au document:

getJSON('story.json')
.then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download in parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence
      .then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

Et voilà, le meilleur des deux ! Le délai de livraison est le même tout le contenu, mais l'utilisateur reçoit le premier contenu plus tôt.

Dans cet exemple trivial, tous les chapitres arrivent à peu près au même moment, mais l'avantage d'un affichage un par un sera exagéré avec des images plus grandes chapitres.

Effectuer la procédure ci-dessus avec des rappels de style Node.js ou événements doubler le code, mais surtout n'est pas aussi facile à suivre. Toutefois, En association avec d'autres fonctionnalités d'ES6, l'histoire ne s'arrête pas là. ils deviennent encore plus faciles.

Bonus: capacités étendues

Depuis que j'ai rédigé cet article, la possibilité d'utiliser des promesses s'est élargie considérablement. Depuis Chrome 55, les fonctions asynchrones permettent d'utiliser un code basé sur des promesses écrit comme s'il était synchrone, mais sans bloquer le thread principal. Vous pouvez Pour en savoir plus, consultez l'article Mes fonctions asynchrones. Il y a Compatibilité généralisée avec les promesses et les fonctions asynchrones dans les principaux navigateurs. Vous trouverez plus de détails dans les MDN Promesse et asynchrone référence.

Un grand merci à Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans et Yutaka Hirano qui ont relu cela et ont fait de corrections/recommandations.

Merci également à Mathias Bynens pour Mettre à jour différentes parties de l'article.