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