Skip to content

Contrôle de flux asynchrone

INFO

Le contenu de cet article est fortement inspiré du livre Node.js de Mixu.

Au cœur de son architecture, JavaScript est conçu pour être non bloquant sur le thread principal, celui où les vues sont rendues. On peut imaginer l'importance de cela dans un navigateur. Lorsque le thread principal est bloqué, cela entraîne le fameux "gel" que redoutent les utilisateurs finaux, et aucun autre événement ne peut être dispatché, entraînant une perte d'acquisition de données, par exemple.

Cela crée des contraintes uniques que seul un style de programmation fonctionnel peut résoudre. C'est là qu'interviennent les callbacks.

Cependant, les callbacks peuvent devenir difficiles à gérer dans des procédures plus complexes. Cela entraîne souvent un "enfer de callbacks" où de multiples fonctions imbriquées avec des callbacks rendent le code plus difficile à lire, à déboguer, à organiser, etc.

js
async1(function (input, result1) {
  async2(function (result2) {
    async3(function (result3) {
      async4(function (result4) {
        async5(function (output) {
          // faire quelque chose avec output
        });
      });
    });
  });
});

Bien sûr, dans la vie réelle, il y aurait probablement des lignes de code supplémentaires pour gérer result1, result2, etc., ainsi, la longueur et la complexité de ce problème entraînent généralement un code beaucoup plus désordonné que l'exemple ci-dessus.

C'est là que les fonctions sont d'une grande utilité. Des opérations plus complexes sont composées de nombreuses fonctions :

  1. style initiateur / entrée
  2. middleware
  3. terminateur

Le "style initiateur / entrée" est la première fonction de la séquence. Cette fonction acceptera l'entrée originale, le cas échéant, pour l'opération. L'opération est une série de fonctions exécutables, et l'entrée originale sera principalement :

  1. des variables dans un environnement global
  2. une invocation directe avec ou sans arguments
  3. des valeurs obtenues par des requêtes système de fichiers ou réseau

Les requêtes réseau peuvent être des requêtes entrantes initiées par un réseau étranger, par une autre application sur le même réseau, ou par l'application elle-même sur le même réseau ou un réseau étranger.

Une fonction middleware retournera une autre fonction, et une fonction terminateur appellera le callback. Ce qui suit illustre le flux vers les requêtes réseau ou système de fichiers. Ici, la latence est de 0 car toutes ces valeurs sont disponibles en mémoire.

js
function final(someInput, callback) {
  callback(`${someInput} et terminée par l'exécution du callback `);
}
function middleware(someInput, callback) {
  return final(`${someInput} touché par le middleware `, callback);
}
function initiate() {
  const someInput = 'bonjour ceci est une fonction ';
  middleware(someInput, function (result) {
    console.log(result);
    // nécessite que le callback `retourne` le résultat
  });
}
initiate();

Gestion de l'état

Les fonctions peuvent ou non dépendre de l'état. La dépendance à l'état survient lorsque l'entrée ou une autre variable d'une fonction dépend d'une fonction externe.

De cette manière, il existe deux stratégies principales pour la gestion de l'état :

  1. passer des variables directement à une fonction, et
  2. acquérir une valeur de variable à partir d'un cache, d'une session, d'un fichier, d'une base de données, d'un réseau ou d'une autre source externe.

Notez que je n'ai pas mentionné les variables globales. La gestion de l'état avec des variables globales est souvent un anti-pattern négligé qui rend difficile ou impossible la garantie de l'état. Les variables globales dans les programmes complexes doivent être évitées autant que possible.

Contrôle du flux

Si un objet est disponible en mémoire, l'itération est possible, et il n'y aura pas de changement de contrôle de flux :

js
function getSong() {
  let _song = '';
  let i = 100;
  for (i; i > 0; i -= 1) {
    _song += `${i} beers on the wall, you take one down and pass it around, ${
      i - 1
    } bottles of beer on the wall\n`;
    if (i === 1) {
      _song += "Hey let's get some more beer";
    }
  }
  return _song;
}
function singSong(_song) {
  if (!_song) throw new Error("song is '' empty, FEED ME A SONG!");
  console.log(_song);
}
const song = getSong();
// ceci fonctionnera
singSong(song);

Cependant, si les données existent en dehors de la mémoire, l'itération ne fonctionnera plus :

js
function getSong() {
  let _song = '';
  let i = 100;
  for (i; i > 0; i -= 1) {
    /* eslint-disable no-loop-func */
    setTimeout(function () {
      _song += `${i} beers on the wall, you take one down and pass it around, ${
        i - 1
      } bottles of beer on the wall\n`;
      if (i === 1) {
        _song += "Hey let's get some more beer";
      }
    }, 0);
    /* eslint-enable no-loop-func */
  }
  return _song;
}
function singSong(_song) {
  if (!_song) throw new Error("song is '' empty, FEED ME A SONG!");
  console.log(_song);
}
const song = getSong('beer');
// ceci ne fonctionnera pas
singSong(song);
// Uncaught Error: song is '' empty, FEED ME A SONG!

Pourquoi cela s'est-il produit ? setTimeout demande à l'unité centrale de traitement de stocker les instructions ailleurs sur le bus, et indique que les données sont programmées pour être récupérées ultérieurement. Des milliers de cycles d'unité centrale de traitement s'écoulent avant que la fonction ne soit à nouveau atteinte à la marque de 0 milliseconde, l'unité centrale de traitement récupère les instructions du bus et les exécute. Le seul problème est que la chanson ('') a été renvoyée des milliers de cycles auparavant.

La même situation se produit lors de la manipulation des systèmes de fichiers et des requêtes réseau. Le thread principal ne peut tout simplement pas être bloqué pendant une période indéterminée ; par conséquent, nous utilisons des rappels pour planifier l'exécution du code dans le temps de manière contrôlée.

Vous serez en mesure d'effectuer presque toutes vos opérations avec les 3 modèles suivants :

  1. En série : les fonctions seront exécutées dans un ordre séquentiel strict, celui-ci est le plus similaire aux boucles for.
js
// opérations définies ailleurs et prêtes à être exécutées
const operations = [
  { func: function1, args: args1 },
  { func: function2, args: args2 },
  { func: function3, args: args3 },
];
function executeFunctionWithArgs(operation, callback) {
  // exécute la fonction
  const { args, func } = operation;
  func(args, callback);
}
function serialProcedure(operation) {
  if (!operation) process.exit(0); // terminé
  executeFunctionWithArgs(operation, function (result) {
    // continuer APRÈS le rappel
    serialProcedure(operations.shift());
  });
}
serialProcedure(operations.shift());
  1. Parallèle complet : lorsque l'ordre n'est pas un problème, comme l'envoi d'e-mails à une liste de 1 000 000 de destinataires d'e-mails.
js
let count = 0;
let success = 0;
const failed = [];
const recipients = [
  { name: 'Bart', email: 'bart@tld' },
  { name: 'Marge', email: 'marge@tld' },
  { name: 'Homer', email: 'homer@tld' },
  { name: 'Lisa', email: 'lisa@tld' },
  { name: 'Maggie', email: 'maggie@tld' },
];
function dispatch(recipient, callback) {
  // `sendMail` est un client SMTP hypothétique
  sendMail(
    {
      subject: 'Dinner tonight',
      message: 'We have lots of cabbage on the plate. You coming?',
      smtp: recipient.email,
    },
    callback
  );
}
function final(result) {
  console.log(`Result: ${result.count} attempts \
      & ${result.success} succeeded emails`);
  if (result.failed.length)
    console.log(`Failed to send to: \
        \n${result.failed.join('\n')}\n`);
}
recipients.forEach(function (recipient) {
  dispatch(recipient, function (err) {
    if (!err) {
      success += 1;
    } else {
      failed.push(recipient.name);
    }
    count += 1;
    if (count === recipients.length) {
      final({
        count,
        success,
        failed,
      });
    }
  });
});
  1. Parallèle limité : parallèle avec limite, comme l'envoi réussi d'e-mails à 1 000 000 de destinataires à partir d'une liste de 10 millions d'utilisateurs.
js
let successCount = 0;
function final() {
  console.log(`dispatched ${successCount} emails`);
  console.log('finished');
}
function dispatch(recipient, callback) {
  // `sendMail` est un client SMTP hypothétique
  sendMail(
    {
      subject: 'Dinner tonight',
      message: 'We have lots of cabbage on the plate. You coming?',
      smtp: recipient.email,
    },
    callback
  );
}
function sendOneMillionEmailsOnly() {
  getListOfTenMillionGreatEmails(function (err, bigList) {
    if (err) throw err;
    function serial(recipient) {
      if (!recipient || successCount >= 1000000) return final();
      dispatch(recipient, function (_err) {
        if (!_err) successCount += 1;
        serial(bigList.pop());
      });
    }
    serial(bigList.pop());
  });
}
sendOneMillionEmailsOnly();

Chacun a ses propres cas d'utilisation, avantages et problèmes que vous pouvez expérimenter et sur lesquels vous pouvez en apprendre davantage. Plus important encore, n'oubliez pas de modulariser vos opérations et d'utiliser des rappels ! Si vous avez le moindre doute, traitez tout comme s'il s'agissait d'un middleware !