LIFWEB - CM4

Programmation Asychrone

Romuald THION

Semestre printemps 2023-2024 UCBL

TP Gestion de galerie d’images

Alignement des nommages

Retours sur les productions

  • 📖 conventionner le code : nommage et formatage
    • bientôt des outils pour cela.
  • ⚠️ ne pas maintenir d’état ni de numéros des <div>
    • le DOM maintient déjà le tableau des enfants !
  • 🔁 Don’t Repeat Yourself (DRY)
  • 🚨 trouver le <div> depuis le handler :
    • via this.parent
    • via event.target.parent
    • via délégation d’événement
    • via closure 👈

Démonstration

Solution du TP avec MutationObserver (BONUS 1) et <template> (BONUS 2) (fichiers galerie.html, galerie.js et galerie.css).

Test de qualité ✅

  • déplacer les boutons dans le <div>
  • ajouter un <div> autour des boutons
  • rajouter des boutons top et bottom

Introduction

Joaquin Phoenix se prépare pour Joker

Trois grandes approches pour les traitements asynchrones :

  • 👀 callbacks : explicites, universels (à la Node.js) ;
  • 👉 promesses : chaînage pour éviter callback hell ;
  • 👉 async/await : simplification sur les promesses.

💡 Pour comprendre les promesses, il faut comprendre les callbaks et le style CPS.

💡 Pur comprendre async/await il faut comprendre les promesses.

Conventions des callbacks en Node.js

  • Le callback est le dernier paramètre
  • Le callback prend lui-même deux paramètres appelés par convention :
    • error : vaut null en cas de succès.
    • value ou data : valeur de retour.
asyncFunction(params, callback);

function callback(error, value) {
  if (error) {
    console.error(error);
    // traitement
    return; // ⚠️ arrête le flot
  }
  console.log(value);
  // traiter value
}

Exemple : l’API FS (via callbacks)

import { readFile } from "node:fs";
const mypath = "async-download.html";

readFile(mypath, "utf8", function (error, data) {
  // 💡 early return on error
  if (error) {
    return console.error(error);
  }
  console.log(">>>");
  console.log(data);
  console.log("<<<");
});

Voir exemple et l’API node:fs.

Promesses

Explain promises

Introduction aux promesses

L’interface Promise représente un intermédiaire (proxy) vers une valeur qui n’est pas nécessairement connue au moment de sa création. Ainsi, des méthodes asynchrones renvoient des valeurs comme les méthodes synchrones, la seule différence est que la valeur retournée par la méthode asynchrone est une promesse (d’avoir une valeur plus tard).

MDN

Passage des callbacks aux promesses

  • Promise introduit en ES2015+ (MDN)
  • On ne passe plus le callback en paramètre de la fonction asynchrone
    • asyncFunc(...params, cb)
  • On passe le callback à l’objet Promise renvoyé par la fonction :
    • la promesse aura la responsabilité de l’exécution du callback dans le futur ;
    • la promesse est renvoyée en synchrone.
    • asyncFunc(...params).then(cb)

États des promesses

Trois états possibles :
  • 💤 pending : en attente, ni remplie, ni rompue ;
  • fulfilled : promesse tenue, l’opération a réussi ;
  • rejected : promesse rompue, l’opération a échoué.

Construction de promesses

🤯 Pour créer une Promise, on donne une fonction d’ordre supérieure qui elle-même prend deux fonctions en paramètres, nommées (par convention) :

  • resolve qui va résoudre en succès ;
  • reject qui va résoudre en échec.
const promise = new Promise(function (resolve, reject) {
  if (condition) {
    resolve(value); // ✅ succès
  } else {
    reject(reason); // ❎ échec
  }
});

💡 Il est assez rare de créer directement des promesses via le constructeur.

⚠️ L’environnement fixe resolve et reject (code natif) et appelle l’exécuteur immédiatement (exemple).

console.log("A");
const promise = new Promise(function (resolve, reject) {
  console.info(`resolve = ${resolve}`);
  console.info(`reject = ${reject}`);
  resolve(42);
});
console.log(promise);
console.log("B");
promise.then(console.log);
console.log("C");

☠️ C’est assez piégeux. Voir l’exemple. ☠️

Chaîner les promesses

La solution au callback hell le modèle objet à la rescousse ! (source)
Les méthodes .then et .catch renvoient des promesses qui peuvent être chaînées à leurs tours.

Le chainage donne un style linéaire qui évite l’imbrication des traitements (a.k.a., pyramid of doom).

function do(
    v0 => f1(v0,
        v1 => f2(v1,
            v2 => f3(v2,
                v3 => f4(v3)
            )
        )
    )
);
p.then((v0) => f1(v0))
  .then((v1) => f2(v1))
  .then((v2) => f3(v2))
  .then((v3) => f4(v3));

Méthode .then()

Promise.then(fn) renvoie toujours une promesse
  • Soit fn retourne une promesse R dont le résultat sera utilisé quand elle sera résolue ;
  • Soit fn retourne une valeur R qui sera transformée automatiquement en une promesse immédiatement résolue.

Méthodes .then(), .catch() et .finally()

promise
  .then((value) => {
    /* ✅ fulfillment */
    /* produit par un appel à `resolve` dans l'exécuteur */
  })
  .catch((error) => {
    /* ❎ rejection */
    /* produit par un appel à `reject` dans l'exécuteur */
  })
  .finally(() => {
    /* 💡 dans tous les cas  : pas de paramètre*/
  });

Exemple de chaînage avec .then(), .catch() et .finally()

console.info("Start");
Promise.resolve(1)
  .then(inc)
  .then((_) => Promise.reject(new Error("Broken")))
  .then(inc) // ⚠️ n'est pas exécuté
  .catch((err) => {
    console.error(err);
    return 42;
  }) // ⚠️ le traitement normal reprend
  .then(inc)
  .finally(() => console.log("Done"));
console.info("End");

❓ Qu’est-ce qui s’affiche ? (exemple)

Remarques sur .then

  • ⚠️ Au cas où fn n’est pas une fonction :
    • 🪳 La promesse d’origine est renvoyée : à bannir
  • 💡 Si la promesse est déjà résolue :
    • on peut encore chaîner un traitement .then;
    • il sera déclenché au prochain tour.
Promise.resolve(42).then(1).then(console.log);
// 🪳 affiche 42 car 1 n'est **pas** une fonction
const p = Promise.resolve(42);
p.then(() => p.then(() => console.log(0)));
// 💡 affichera bien 0, même si 42 était déjà résolue

⚠️ Ici pour l’exemple, on a imbriqué les promesses, mais c’est à éviter, voir promise/no-nesting. ⚠️

Promisification

🤯 La promisification consiste à transformer une API par callback en une API qui renvoie une Promise.

function promisify(funct) {
  // 💡 wrapper (*) returns a Promise
  return function (...args) {
    return new Promise((resolve, reject) => {
      // custom callback/executor (**)
      function callback(err, result) {
        if (err) reject(err); // ❎
        else resolve(result); // ✅
      }
      funct.call(this, ...args, callback);
      // append custom callback and call the original function
    });
  };
}

Voir exemple.

Exemple avec : promisification de setTimeout

// API callback traditionnelle
const waitCallback = (delay, value, cb) => setTimeout(() => cb(null, value), delay);
const logCallback = (err, res) => (err ? console.error(err) : console.log("waitCallback", res));
waitCallback(1000, 42, logCallback);

// Promisification manuelle
const waitPromise = promisify(waitCallback);
waitPromise(1000, 42)
  .then((v) => console.log("waitPromise", v))
  .catch(console.error);

Voir exemple.

La bibliothèque standard Node.js propose les outils util.promisify et util.callbackify.

// Promisification via promisify standard
const waitPromiseNode = nodePromisify(waitCallback);
waitPromiseNode(1000, 42)
  .then((v) => console.log("waitPromiseNode", v))
  .catch(console.error);

// Via nouvelle API Promisifiée de setTimeout
const waitNative = (delay, value) => setTimeoutPromise(delay, value);
waitNative(1000, 42)
  .then((v) => console.log("waitNative", v))
  .catch(console.error);

⚠️ Si vous devez promisifier, use the platform. ⚠️ Voir exemple.

Exemple : l’API FS (via promesses)

// 💡 Ici, l'API native promisifiée
import { readFile } from "node:fs/promises";
const mypath = "async-download.html";

readFile(mypath, { encoding: "utf8" })
  .then(function (data) {
    console.log(">>>");
    console.log(data);
    console.log("<<<");
  })
  .catch(console.error);

Voir exemple et l’API node:fs.

Méthodes de la classe Promise

💡 Méthodes de classe, pas d’instances

Promesses concurrentes (1/2)

The Promise.all() method returns a single Promise that resolves when all of the promises in the iterable argument have resolved.

The Promise.allSettled() method returns a promise that fulfills after all of the given promises have either fulfilled or rejected.

Exemple : promesses parallèles
Promise.all([
  new Promise((resolve) => setTimeout(() => resolve(1), 0)),
  new Promise((resolve) => setTimeout(() => resolve(2), 1000)),
  new Promise((resolve) => setTimeout(() => resolve(3), 500)),
]).then(console.info);

💡 Affiche [1, 2, 3] après une seconde et garanti l’ordre dans le tableau. Voir l’exemple.

Promesses concurrentes (2/2)

Promise.any() takes an iterable of Promise objects. It returns a single promise that fulfills as soon as any of the promises in the iterable fulfills, with the value of the fulfilled promise.

The Promise.race() method returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects

Promesses immédiatement résolues ou rejetées

De façon simplifiée, fonctionnellement équivalent :

const resolve = (value) => new Promise((resolve, reject) => resolve(value));
const reject = (error) => new Promise((resolve, reject) => reject(error));

resolve(42).then(console.log);
reject(42).catch(console.error);

async / await

Want to learn JS?

Faciliter l’utilisation des promesses

  • async / await introduits dans ES2017+ (MDN)
  • Une fonction async renvoie toujours une promesse (synchrone) ;
  • await p attend la résolution de la promesse
    • 💡 Suspend l’exécution jusqu’à la résolution.
    • Possible au top-level en module (voir TC39).
  • La gestion d’erreur se fait comme pour les exceptions synchrones
    • avec les blocs try, catch et finally

Réécriture async / await

async function f() {
  return 1;
}
const r = await f();
console.log(r);

💡 await correspond moralement à une ré-écriture du traitement dans le then des Promise.

function f() {
  return Promise.resolve(1);
}
f().then((r) => console.log(r));

❗ Pour maîtriser async / await, il faut maîtriser les Promise !

Exemple avec setTimeout

function timeout(ms) {
  return new Promise((resolve) => setTimeout(() => resolve("Get Up!"), ms));
}
async function sleep() {
  const r = await timeout(3000);
  console.info(r);
  return r;
}
console.log("Before");
await sleep(); // ⚠️ await SUSPEND l'EXECUTION ⚠️
console.log("After");

⚠️ Ne pas mettre des await au 🎲 ! Voir l’exemple.

Exemple : l’API FS (via async/await)

import { readFile } from "node:fs/promises";
const mypath = "async-download.html";

try {
  const data = await readFile(mypath, { encoding: "utf8" });
  console.log(">>>");
  console.log(data);
  console.log("<<<");
} catch (error) {
  console.error(error);
}

👍 Respecte le guide de style. Voir exemple.

API fetch

The Fetch API provides an interface for fetching resources (including across the network). It will seem familiar to anyone who has used XMLHttpRequest, but the new API provides a more powerful and flexible. (MDN)

Principaux objets de la Fetch API

  • Fonction fetch
    • Principal point d’entrée
  • Classe Request
    • Transmise par fetch
  • Classe Response
    • Reçue par fetch
    • propriétés ok, headers, status, etc.
    • méthodes async json(), text(), arrayBuffer(), formData(), etc.
      • pour accéder au corps en différents formats
  • Classe Headers

Exemple avec fetch

fetch(resource, options)
  .then((resp) => resp.json())
  .then((body) => console.info(body.length));
  • Paramètre ressource de type string, URL ou Request.
  • Pour une requête POST, PUT ou PATCH, le body est passé dans les options, avec le type de méthode, l’entête, etc.

Traduction async / Promise

Style Promise

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error("fetch failed", err);
    });
}

Style async/await

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  } catch (err) {
    console.log("fetch failed", err);
  }
}

Conclusion

Sur les promesses

  • 👍 Standardisées depuis ES2015+.
  • 👍 Facilitent l’enregistrement de handlers :
    • en les passant aux résultats (synchrones) des fonctions et non en paramètre des appels.
  • 👍 Donnent un style linéaire au code.
  • 👍 Les méthodes de classes pour les besoins courants.

Attention aux fausses promesses

  • ⚠️ L’exécution reste toujours asynchrone
    • Il faut toujours passer des fonctions en paramètre :
      • juste différement
    • Il faut souvent créer des fermetures
  • ⚠️ Les promesses ne permettent qu’un seul appel à reject/resolve dans l’exécuteur
    • plusieurs appels possible en revanche pour les callbacks style Node.js