const yesNoAPIurl = "https://yesno.wtf/api" const DEFAULT_DELAI = 1000; /** * Cette fonction peut être fournie dans le .then d'une promesse. * Elle accepte n'importe quels paramètres et les transmet après un délai de * ms millisecondes. */ function wait(ms, number) { console.log("number =", number, "ms =", ms); return function(...v) { return new Promise(resolve => setTimeout(() => resolve(...v), ms)); }; } let yesNoRequestCount = 0; // pour numéroter les appels à yesNo /** * Retourner la réponse json de yesno.wtf sous la forme d'une promesse. * Un délai aléatoire compris entre 0 et `delai` et ajouté avant de rendre le * résultat. */ function yesNo(delai) { return yesNoThen(delai); } // Avec .then function yesNoThen(delai) { delai = Number(delai) || DEFAULT_DELAI; const number = yesNoRequestCount++; return fetch(yesNoAPIurl) .then(wait(Math.random() * delai, number)) .then(response => response.json()) .then(object => ({...object, number: number})); } // Avec await. async function yesNoAwait(delai) { delai = Number(delai) || DEFAULT_DELAI; const number = yesNoRequestCount++; const response = await fetch(yesNoAPIurl); await wait(Math.random() * delai, number)(); const object = await response.json(); return {...object, number: number} } /** * Créer un élement img à partir de sa source `src` et de son texte alternatif * `alternate`. */ function imgElement(src, alternate) { const img = document.createElement('img'); img.setAttribute('src', src); img.setAttribute('alternate', alternate); return img; } /** * Créer un titre de niveau 2 (h2). */ function h2Element(titre) { const h2 = document.createElement('h2'); h2.append(titre); return h2; } /** * Ajouter à la fin du body, une section avec l'image retournée par yesNo. */ function oneYesNoThen() { const section = document.createElement('section'); const article = document.createElement('article'); section.append(article); document.body.append(section); // On se place déjà dans le document yesNo().then(data => { const number = h2Element(data.number); const img = imgElement(data.image, data.answer); const txt = h2Element(data.answer) article.append(number, img, txt); }); } async function oneYesNoAwait() { const section = document.createElement('section'); const article = document.createElement('article'); section.append(article); document.body.append(section); // On se place déjà dans le document const data = await yesNo(); const number = h2Element(data.number); const img = imgElement(data.image, data.answer); const txt = h2Element(data.answer) article.append(number, img, txt); } // Conclusion : La version await est plus naturelle, on a style de // programmation classique où l'on appelle une fonction et on obtient son // résultat utilisé dans la suite des calculs. /** * Ajouter à la fin du body une section contenant `count` articles, chaque * article correspondant à une réponse de yesno. Un article comprend son * numéro, l'image yesno et la réponse. */ // Commençons avec un for classique et une version then... function severalYesNoForClassiqueThen(count) { const section = document.createElement('section'); document.body.append(section); // pour voir tout de suite l'effet for (let i = 0; i < count; i++) { // Créer un nouvelle article avec le numéro d'ordre et la réponse yesNo const article = document.createElement('article'); section.appendChild(article); // Ajouter le numéro d'ordre article.append(h2Element('' + i)); // Ajouter la réponse yesNo().then( response => { const img = imgElement(response.image, response.answer); const text = h2Element(response.answer); article.append(img, text); }); } } // Tout marche bien. Dans un délai d'environ DEFAULT_DELAI max, toutes les // images sont affichées. Bien sûr pas forcément dans l'ordre : ceci dépend du // délai pour les obtenir. // Faisons la même chose avec await. On garde la même structure. // Que constate-t-on lors de l'éxécution ? async function severalYesNoForClassiqueAwait(count) { const section = document.createElement('section'); document.body.append(section); // pour voir tout de suite l'effet for (let i = 0; i < count; i++) { // Créer un nouvelle article avec le numéro d'ordre et la réponse yesNo const article = document.createElement('article'); section.appendChild(article); // Ajouter le numéro d'ordre article.append(h2Element('' + i)); // Ajouter la réponse const response = await yesNo(); const img = imgElement(response.image, response.answer); const text = h2Element(response.answer); article.append(img, text); } } // Ça fonctionne mais les images s'affichent les unes après les autres de la // première à la dernière. Le temps total de chargement des images est la // somme des temps de récupération de chaque image (y compris la // temporisation). Pas très satisfaisant. Le await attend que la promesse se // réalise avant de passer à la suite. Ce n'est pas vraimeent satisfaisant // ici. // Si on ne veut pas que ce soit bloquant, il faut exécuter la partie "Ajouter // les réponse" de manière asynchrone... En l'encapsulant dans une promesse, // donc une fonction asyncrhone que l'on appelle tout de suite ! async function severalYesNoForClassiqueAwaitFonctionAsynchrone(count) { const section = document.createElement('section'); document.body.append(section); // pour voir tout de suite l'effet for (let i = 0; i < count; i++) { // Créer un nouvelle article avec le numéro d'ordre et la réponse yesNo const article = document.createElement('article'); section.appendChild(article); // Ajouter le numéro d'ordre article.append(h2Element('' + i)); // Ajouter la réponse async function ajouterReponse () { const response = await yesNo(); const img = imgElement(response.image, response.answer); const text = h2Element(response.answer); article.append(img, text); } ajouterReponse(); } } // Ça marche ! On retrouve bien le même comportement qu'avec les then. Mais // moins naturel que de l'async pur. // Mais pourquoi nommer cette fonction ajouterReponse alors qu'on ne s'en sert // qu'une seule fois. Utilisons une fonction anonyme utilisée immédiatement. async function severalYesNoForClassiqueAwaitFonctionAsynchroneAnonyme(count) { const section = document.createElement('section'); document.body.append(section); // pour voir tout de suite l'effet for (let i = 0; i < count; i++) { // Créer un nouvelle article avec le numéro d'ordre et la réponse yesNo const article = document.createElement('article'); section.appendChild(article); // Ajouter le numéro d'ordre article.append(h2Element('' + i)); // Ajouter la réponse (async function () { const response = await yesNo(); const img = imgElement(response.image, response.answer); const text = h2Element(response.answer); article.append(img, text); })(); } } // On a bien le même comportement. // Lisible les fonctions anonymes ? // Et on avait vu map, filter, forEach... Pourrait-on s'en servir ? // Commençons par définir une fonction qui fait l'équialent de // `list(range(count))` de Python. function listRange(count) { l = [] for (let i = 0; i < count; i++) { l.push(i); } return l; } console.log(listRange(10)); // Que l'on peut aussi écrire function listRange(count) { return [...Array(count).keys()] } console.log(listRange(10)); // Array(5).keys() n'est pas un tableau mais un Iterator (itérateur). console.log(Array(10).keys()) // Array.from permet d'obtenir un tableau classique à partir d'un itérateur console.log(Array.from(Array(10).keys())) // On peut aussi utiliser l'opérateur ... console.log([...Array(10).keys()]) // à partir de la liste des entiers de 0 à count-1, on peut créer les `count` // articles avec leur numéro d'ordre (`map`) puis pour chaque article ajouter // la réponse de yesNo. function severalYesNo(count) { // Ajouter à body une section avec `count` articles const section = document.createElement('section'); const articles = // les élément
listRange(count) // les entiers de 0 à count-1 .map( i => { const article = document.createElement('article'); article.appendChild(h2Element('' + i)); return article; }); section.append(...articles); document.body.appendChild(section); // pour voir tout de suite l'effet sur la page. // Ajouter les réponses de yesno articles.forEach( article => { yesNo().then( response => { const img = imgElement(response.image, response.answer); const txt = h2Element(response.answer); article.append(img, txt); }); }); } // Ça marche... // Et avec await, on peut retrouver les mêmes problèmes que précédemment. async function severalYesNoAwaitBad(count) { // Ajouter à body une section avec `count` articles const section = document.createElement('section'); const articles = // les élément
listRange(count) // les entiers de 0 à count-1 .map( i => { const article = document.createElement('article'); article.appendChild(h2Element('' + i)); return article; }); section.append(...articles); document.body.appendChild(section); // pour voir tout de suite l'effet sur la page. // Ajouter les réponses de yesno for (const article of articles) { const response = await yesNo(); const img = imgElement(response.image, response.answer); const text = document.createElement("h2"); text.append(response.answer); article.append(img, text); } } // La preuve : les requêtes à yesNo sont traitées en séquence ! async function severalYesNoAwaitGood(count) { // Ajouter à body une section avec `count` articles const section = document.createElement('section'); const articles = // les élément
listRange(count) // les entiers de 0 à count-1 .map( i => { const article = document.createElement('article'); article.appendChild(h2Element('' + i)); return article; }); section.append(...articles); document.body.appendChild(section); // pour voir tout de suite l'effet sur la page. // Ajouter les réponses de yesno articles.forEach( async article => { const response = await yesNo(); const img = imgElement(response.image, response.answer); const text = document.createElement("h2"); text.append(response.answer); article.append(img, text); }); } // De nouveau le comportement attendu... En utilisant une fonction asynchrone // dans le forEach. /** * Number of yes and number of no for `count` calls to yesno. */ function yesNoCountThenBad(count) { let yesCount = 0; let noCount = 0; for (let i = 0; i < count; i++) { yesNo().then(response => { if (response.answer === 'yes') { yesCount++; } else if (response.answer === 'no') { noCount++; } }); } return {yes: yesCount, no: noCount}; } // Remarque : parce que les fonctions en javascript sont exécutées en mode "run // to completion" (elles s'exécutent jusqu'à ce qu'elles se terminent) il n'y a // pas de risque d'accès concurrent aux variables yesCount et noCount. Chaque // fonction du then (et donc chaque incrémentation) sera exécutée seule et // complètement. // Est-ce que le résultat renvoyé est le bon ? // Non, on a toujours {yes: 0, no: 0}. Pourquoi ? // Parce que le résultat est retourné avant que les then des yesNo aient pu // s'exécuter... // Et avec async ? async function yesNoCountAwaitBad(count) { let yesCount = 0; let noCount = 0; for (let i = 0; i < count; i++) { const {answer} = await yesNo(); yesCount += answer === "yes"; noCount += answer === "no"; } return {yes: yesCount, no: noCount}; } // On obtient le bon résultat... Mais c'est très lent car on attend la // résolution d'un yesNo() avant de lancer le suivant. // Remarque : le compte des 'yes' et 'no' marche car, dans une expression // arithmétique, un booléen s'évalue à 1 s'il est vrai et à 0 s'il est faux. // // Dans : // const {answer} = await yesNo(); // on ne récupère que la valeur de l'attribut `answer` de l'objet retourné par // `await yesNo()` qui sert à initialiser une constante `answer`. // Dans les deux cas, le bon résultat ne sera disponible que quand tous les // yesNo() auront fourni leur réponse (et qu'elle sera donc traitée). Dans la // version .then, on ne sait pas quand. dans la version for, c'est à la sortie // de la boucle mais on a tout exécuté en séquence alors qu'il était possible // de paralléliser les requêtes. // Le principe dans ce cas est de mettre dans un tableau les promesses qui // peuvent s'exécuter un parallèle et attendre qu'elles se terminent. C'est ce // que permet Promise.all (qui crée une nouvelle promesse à partir d'un tableau // de promesse, elle sera résolue quand toutes les promesses du tableau le // seront) et for await. function yesNoCountTabThen(count) { const promesses = listRange(count).map( () => yesNo() ); // Tab de `count` appels à yesNo const toutes = Promise.all(promesses); // Une seule promesse pour les gérer toutes const counts = {yes: 0, no: 0, maybe: 0}; // nos 3 compteurs dans un bojet return toutes .then(responses => responses.forEach(r => counts[r.answer]++)) .then(() => counts); } // Ça marche. Les yesNo() sont bien traités en //. // // Remarque : Le dernier then retourne la valeur de counts et correspond à une // promesse qui est tout de suite résolue. // Et avec await... async function yesNoCountTabAwait(count) { const promesses = listRange(count).map( () => yesNo() ); // Tab de `count` appels à yesNo const counts = {yes: 0, no: 0, maybe: 0}; const responses = await Promise.all(promesses); for (const response of responses) { console.log('dans for:', response); counts[response.answer]++; } return counts; } // Que l'on peut écrire plus simplement avec for await... async function yesNoCountTabForAwait(count) { const promesses = listRange(count).map( () => yesNo() ); // Tab de `count` appels à yesNo const counts = {yes: 0, no: 0, maybe: 0}; for await (const response of promesses) { console.log('dans for await:', response); counts[response.answer]++; } return counts; } // Le for fait un await sur chaque promesse avant de la traiter. // Si la première promesse est prête, la première sera traitée avant que la // deuxième soit résolue. // Dans la version précédente, avec await Promise.all(promesses), le for ne // démarre que quand toutes les promesses sont résolues. // // Et si on veut directement avoir le map des réponses... async function yesNoCountTabForAwaitAnswer(count) { const promesses = listRange(count).map( async () => (await yesNo()).answer); const counts = {yes: 0, no: 0, maybe: 0}; for await (const answer of promesses) { counts[answer]++; } return counts; } // Remarque : attention aux parenthèses autour de `await yesNo()` pour que // `answer` s'applique au résultat. // Et avec plus de map et filter... // Avec .then function yesNoCountTabThenFilter(count) { const promesses = listRange(count).map( () => yesNo() ); // Tab de `count` appels à yesNo return Promise.all(promesses) .then(responses => responses.map(response => response.answer)) .then(answers => ({ yes: answers.filter(a => a === 'yes').length, no: answers.filter(a => a === 'no').length, maybe: answers.filter(a => a === 'maybe').length, })); } // Et avec await... async function yesNoCountTabAwaitFilter(count) { const promesses = listRange(count).map( () => yesNo() ); const responses = await Promise.all(promesses); const answers = responses.map( r => r.answer); return { yes: answers.filter(a => a === 'yes').length, no: answers.filter(a => a === 'no').length, maybe: answers.filter(a => a === 'maybe').length, }; } // On crée le tableau des promesse à la main et on fait un await sur // Promise.all. Le for ne démarre alors que quand toutes les promesses sont // résolues. async function yesNoCountAwaitFor(count) { const promesses = []; for (let i = 0; i < count; i++) { promesses.push(yesNo()); } const counts = {yes: 0, no: 0, maybe: 0}; const reponses = await Promise.all(promesses) for (const reponse of reponses) { counts[reponse.answer]++; } return counts; } // window.addEventListener("load", oneYesNoThen); window.addEventListener("load", oneYesNoAwait); // window.addEventListener("load", () => severalYesNoForClassiqueThen(4)); // window.addEventListener("load", () => severalYesNoForClassiqueAwait(4)); // window.addEventListener("load", () => severalYesNoForClassiqueAwaitFonctionAsynchrone(4)); // window.addEventListener("load", () => severalYesNoForClassiqueAwaitFonctionAsynchroneAnonyme(4)); // window.addEventListener("load", () => severalYesNo(4)); // window.addEventListener("load", () => severalYesNoAwaitBad(4)); // window.addEventListener("load", () => severalYesNoAwaitGood(4)); const COUNT = 10; // console.log(`yes/no count ${COUNT} (yesNoCountThenBad) =`, yesNoCountThenBad(COUNT)); // yesNoCountAwaitBad(COUNT).then(ynCount => console.log(`yes/no count ${COUNT} (yesNoCountAwaitBad) =`, ynCount)); // yesNoCountTabThen(COUNT).then(ynCount => console.log(`yes/no count ${COUNT} (yesNoCountTabThen) =`, ynCount)); // yesNoCountTabAwait(COUNT).then(ynCount => console.log(`yes/no count ${COUNT} (yesNoCountTabAwait) =`, ynCount)); // yesNoCountTabForAwait(COUNT).then(ynCount => console.log(`yes/no count ${COUNT} (yesNoCountTabForAwait) =`, ynCount)); // yesNoCountTabForAwaitAnswer(COUNT).then(ynCount => console.log(`yes/no count ${COUNT} (yesNoCountTabForAwaitAnswer) =`, ynCount)); // yesNoCountTabThenFilter(COUNT).then(ynCount => console.log(`yes/no count ${COUNT} (yesNoCountTabThenFilter) =`, ynCount)); // yesNoCountTabAwaitFilter(COUNT).then(ynCount => console.log(`yes/no count ${COUNT} (yesNoCountTabAwaitFilter) =`, ynCount)); // yesNoCount(COUNT).then(ynCount => console.log(`yes/no count ${COUNT} (promise) =`, ynCount)); // yesNoCountAwaitFor(COUNT).then(ynCount => console.log(`yes/no count ${COUNT} (with await for) =`, ynCount));