Fermer

novembre 29, 2022

Sauvé de l’enfer des rappels –

Sauvé de l’enfer des rappels –


L’enfer des rappels est réel. Les développeurs voient souvent les rappels comme un pur mal, au point même de les éviter. La flexibilité de JavaScript n’aide pas du tout avec cela. Mais il n’est pas nécessaire d’éviter les rappels. La bonne nouvelle est qu’il existe des étapes simples pour être sauvé de l’enfer des rappels.

Diable debout au-dessus d'un employé de bureau avec des récepteurs suspendus partout

Éliminer les rappels dans votre code revient à amputer une bonne jambe. Une fonction de rappel est l’un des piliers de JavaScript et l’un de ses bons côtés. Lorsque vous remplacez des rappels, vous ne faites souvent qu’échanger des problèmes.

Certains disent que les rappels sont de vilaines verrues et sont la raison d’étudier de meilleures langues. Eh bien, les rappels sont-ils si laids?

Manier des rappels en JavaScript a son propre ensemble de récompenses. Il n’y a aucune raison d’éviter JavaScript car les rappels peuvent se transformer en vilaines verrues. Nous pouvons simplement nous assurer que cela ne se produise pas.

Plongeons-nous dans ce que la programmation sonore a à offrir avec les rappels. Notre préférence est de nous en tenir à Principes SOLIDES et voyons où cela nous mène.

Qu’est-ce que l’enfer du rappel ?

Vous vous demandez peut-être ce qu’est un rappel et pourquoi vous devriez vous en soucier. En JavaScript, un rappeler est une fonction qui agit en tant que délégué. Le délégué s’exécute à un moment arbitraire dans le futur. En JavaScript, la delegation se produit lorsque la fonction de réception appelle le rappel. La fonction réceptrice peut le faire à n’importe quel moment arbitraire de son exécution.

En bref, un rappel est une fonction passée en argument à une autre fonction. Il n’y a pas d’exécution immédiate, puisque la fonction réceptrice décide quand l’appeler. Ce qui suit exemple de code illustre :

function receiver(fn) {
  return fn();
}

function callback() {
  return 'foobar';
}

var callbackResponse = receiver(callback); 

Si vous avez déjà écrit une requête Ajax, vous avez rencontré des fonctions de rappel. Le code asynchrone utilise cette approche, car il n’y a aucune garantie quand le rappel s’exécutera.

Le problème avec les rappels provient du code asynchrone qui dépend d’un autre rappel. On peut utiliser setTimeout pour simuler des appels asynchrones avec des fonctions de rappel.

N’hésitez pas à suivre. Le dépôt est disponible sur GitHubet la plupart des extraits de code proviendront de là pour que vous puissiez jouer le jeu.

Voici la pyramide du destin !

setTimeout(function (name) {
  var catList = name + ',';

  setTimeout(function (name) {
    catList += name + ',';

    setTimeout(function (name) {
      catList += name + ',';

      setTimeout(function (name) {
        catList += name + ',';

        setTimeout(function (name) {
          catList += name;

          console.log(catList);
        }, 1, 'Lion');
      }, 1, 'Snow Leopard');
    }, 1, 'Lynx');
  }, 1, 'Jaguar');
}, 1, 'Panther');

En regardant le code ci-dessus, setTimeout obtient une fonction de rappel qui s’exécute après une milliseconde. Le dernier paramètre alimente simplement le rappel avec des données. C’est comme un appel Ajax, sauf que le retour name paramètre viendrait du serveur.

Il y a un bon aperçu de la fonction setTimeout sur Site Point.

Dans notre code, nous rassemblons une liste de chats féroces via un code asynchrone. Chaque rappel nous donne un seul nom de chat, et nous l’ajoutons à la liste. Ce que nous tentons de réaliser semble raisonnable. Mais étant donné la flexibilité des fonctions JavaScript, c’est un cauchemar.

Fonctions anonymes

Remarquez l’utilisation de fonctions anonymes dans cet exemple précédent. Fonctions anonymes sont des expressions de fonction sans nom qui sont affectées à une variable ou passées en argument à d’autres fonctions.

L’utilisation de fonctions anonymes dans votre code n’est pas recommandée par certains normes de programmation. Il vaut mieux les nommer, alors utilisez function getCat(name){} à la place de function (name){}. Mettre des noms dans les fonctions ajoute de la clarté à vos programmes. Ces fonctions anonymes sont faciles à taper, mais elles vous envoient sur une autoroute en enfer. Lorsque vous vous retrouvez sur cette route sinueuse d’indentations, il est préférable de s’arrêter et de repenser.

Une approche naïve pour briser ce gâchis de rappels consiste pour nous à utiliser des déclarations de fonction :

setTimeout(getPanther, 1, 'Panther');

var catList = '';

function getPanther(name) {
  catList = name + ',';

  setTimeout(getJaguar, 1, 'Jaguar');
}

function getJaguar(name) {
  catList += name + ',';

  setTimeout(getLynx, 1, 'Lynx');
}

function getLynx(name) {
  catList += name + ',';

  setTimeout(getSnowLeopard, 1, 'Snow Leopard');
}

function getSnowLeopard(name) {
  catList += name + ',';

  setTimeout(getLion, 1, 'Lion');
}

function getLion(name) {
  catList += name;

  console.log(catList);
}

Vous ne trouverez pas cet extrait dans le référentiel, mais l’amélioration incrémentielle peut être trouvée sur cet engagement.

Chaque fonction obtient sa propre déclaration. L’un des avantages est que nous n’obtenons plus l’horrible pyramide. Chaque fonction est isolée et focalisée au laser sur sa propre tâche spécifique. Chaque fonction a maintenant une raison de changer, c’est donc un pas dans la bonne direction. Notez que getPanther(), par exemple, est affecté au paramètre. JavaScript ne se soucie pas de la façon dont nous créons les rappels. Mais quels sont les inconvénients ?

Pour une ventilation complète des différences, voir ceci Article de SitePoint sur les expressions de fonction par rapport aux déclarations de fonction.

Un inconvénient, cependant, est que chaque déclaration de fonction n’est plus portée à l’intérieur du rappel. Au lieu d’utiliser des rappels comme fermeture, chaque fonction est maintenant collée à la portée externe. D’où pourquoi catList est déclaré dans la portée externe, car cela accorde aux rappels l’accès à la liste. Parfois, écraser la portée mondiale n’est pas une solution idéale. Il existe également une duplication de code, car il ajoute un chat à la liste et appelle le rappel suivant.

Ce sont des odeurs de code héritées de l’enfer des rappels. Parfois, s’efforcer d’entrer dans la liberté de rappel nécessite de la persévérance et une attention aux détails. Il peut commencer à se sentir comme si la maladie est mieux que le remède. Existe-t-il un moyen de mieux coder cela?

Inversion de dépendance

Le principe d’inversion de dépendance dit que nous devons coder sur des abstractions, pas sur des détails d’implémentation. À la base, nous prenons un gros problème et le divisons en petites dépendances. Ces dépendances deviennent indépendantes là où les détails de mise en œuvre ne sont plus pertinents.

Ce principe SOLIDE États:

En suivant ce principe, les relations de dépendance conventionnelles établies entre les modules de définition de politique de haut niveau et les modules de dépendance de bas niveau sont inversées, rendant ainsi les modules de haut niveau indépendants des détails de mise en œuvre du module de bas niveau.

Alors, que signifie cette tache de texte ? La bonne nouvelle, c’est qu’en affectant un callback à un paramètre, on le fait déjà ! Au moins en partie, pour être découplé, considérez les rappels comme des dépendances. Cette dépendance devient un contrat. À partir de maintenant, nous faisons de la programmation SOLID.

Une façon d’obtenir la liberté de rappel est de créer un contrat :

fn(catList);

Cela définit ce que nous prévoyons de faire avec le rappel. Il doit suivre un seul paramètre, c’est-à-dire notre liste de chats féroces.

Cette dépendance peut désormais être alimentée via un paramètre :

function buildFerociousCats(list, returnValue, fn) {
  setTimeout(function asyncCall(data) {
    var catList = list === '' ? data : list + ',' + data;

    fn(catList);
  }, 1, returnValue);
}

Notez que l’expression de la fonction asyncCall est limité à la fermeture buildFerociousCats. Cette technique est puissante lorsqu’elle est associée à des rappels dans la programmation asynchrone. Le contrat s’exécute de manière asynchrone et gagne le data il a besoin, le tout avec une programmation sonore. Le contrat gagne la liberté dont il a besoin à mesure qu’il est découplé de la mise en œuvre. Un beau code utilise la flexibilité de JavaScript à son propre avantage.

Le reste de ce qui doit arriver devient évident. Nous pouvons le faire:

buildFerociousCats('', 'Panther', getJaguar);

function getJaguar(list) {
  buildFerociousCats(list, 'Jaguar', getLynx);
}

function getLynx(list) {
  buildFerociousCats(list, 'Lynx', getSnowLeopard);
}

function getSnowLeopard(list) {
  buildFerociousCats(list, 'Snow Leopard', getLion);
}

function getLion(list) {
  buildFerociousCats(list, 'Lion', printList);
}

function printList(list) {
  console.log(list);
}

Il n’y a pas de duplication de code ici. Le rappel garde désormais une trace de son propre état sans variables globales. Un rappel tel que getLion peut être enchaîné avec tout ce qui suit le contrat – c’est-à-dire toute abstraction qui prend une liste de chats féroces comme paramètre. Cet exemple de code est disponible sur GitHub.

Rappels polymorphes

Bon, soyons un peu fous. Et si nous voulions changer le comportement de la création d’une liste séparée par des virgules à une liste délimitée par des tubes ? Un problème que nous pouvons envisager est que buildFerociousCats a été collé à un détail d’implémentation. Notez l’utilisation de list + ',' + data pour faire ça.

La réponse simple est un comportement polymorphe avec rappels. Le principe demeure : traiter les rappels comme un contrat et rendre la mise en œuvre non pertinente. Une fois que le rappel s’élève à une abstraction, les détails spécifiques peuvent changer à volonté.

Le polymorphisme ouvre de nouvelles voies de réutilisation du code en JavaScript. Pensez à un polymorphe callback comme moyen de définir un contrat strict, tout en laissant suffisamment de liberté pour que les détails de mise en œuvre n’aient plus d’importance. Notez que nous parlons toujours d’inversion de dépendance. Un rappel polymorphe est juste un nom fantaisiste qui indique une façon d’aller plus loin dans cette idée.

Définissons le contrat. Nous pouvons utiliser le list et data paramètres de ce contrat :

cat.delimiter(cat.list, data);

Ensuite, nous pouvons apporter quelques modifications à buildFerociousCats:

function buildFerociousCats(cat, returnValue, next) {
  setTimeout(function asyncCall(data) {
    var catList = cat.delimiter(cat.list, data);

    next({ list: catList, delimiter: cat.delimiter });
  }, 1, returnValue);
}

L’objet JavaScript cat englobe désormais le list données et delimiter fonction. La next chaînes de rappel rappels asynchrones – anciennement appelés fn. Notez qu’il est possible de regrouper les paramètres à volonté avec un objet JavaScript. La cat l’objet attend deux clés spécifiques — list et delimiter. Cet objet JavaScript fait désormais partie du contrat. Le reste du code reste le même.

Pour lancer cela, nous pouvons faire ceci:

buildFerociousCats({ list: '', delimiter: commaDelimiter }, 'Panther', getJaguar);
buildFerociousCats({ list: '', delimiter: pipeDelimiter }, 'Panther', getJaguar);

Les rappels sont échangés. Tant que les contrats sont remplis, les détails de mise en œuvre ne sont pas pertinents. Nous pouvons changer le comportement avec facilité. Le rappel, qui est maintenant une dépendance, est inversé en un contrat de haut niveau. Cette idée prend ce que nous savons déjà sur les rappels et l’élève à un nouveau niveau. La réduction des rappels en contrats lève les abstractions et découple les modules logiciels.

Ce qui est si radical ici, c’est que les tests unitaires découlent naturellement de modules indépendants. La delimiter contrat est une fonction pure. Cela signifie que, étant donné un certain nombre d’entrées, nous obtenons la même sortie à chaque fois. Ce niveau de testabilité renforce la confiance que la solution fonctionnera. Après tout, l’indépendance modulaire accorde le droit à l’auto-évaluation.

Un test unitaire efficace autour du délimiteur de tuyau pourrait ressembler à ceci :

describe('A pipe delimiter', function () {
  it('adds a pipe in the list', function () {
    var list = pipeDelimiter('Cat', 'Cat');

    assert.equal(list, 'Cat|Cat');
  });
});

Je vous laisse imaginer à quoi ressemblent les détails de mise en œuvre. N’hésitez pas à consulter le valider sur GitHub.

Promesses

UN promettre est simplement un wrapper autour du mécanisme de rappel, et permet un puisable pour continuer le flux d’exécution. Cela rend le code plus réutilisable car vous pouvez retourner une promesse et enchaîner la promesse.

Construisons au-dessus du rappel polymorphe et enveloppons cela autour d’une promesse. Ajustez le buildFerociousCats fonction et faites-lui retourner une promesse:

function buildFerociousCats(cat, returnValue, next) {
  return new Promise((resolve) => { 
    setTimeout(function asyncCall(data) {
      var catList = cat.delimiter(cat.list, data);

      resolve(next({ list: catList, delimiter: cat.delimiter }));
    }, 1, returnValue);
  });
}

Notez l’utilisation de resolve: au lieu d’utiliser directement le rappel, c’est ce qui résout la promesse. Le code consommateur peut appliquer un then pour continuer le flux d’exécution.

Étant donné que nous renvoyons maintenant une promesse, le code doit garder une trace de la promesse lors de l’exécution du rappel.

Mettons à jour les fonctions de rappel pour renvoyer la promesse :

function getJaguar(cat) {
  return buildFerociousCats(cat, 'Jaguar', getLynx); 
}

function getLynx(cat) {
  return buildFerociousCats(cat, 'Lynx', getSnowLeopard);
}

function getSnowLeopard(cat) {
  return buildFerociousCats(cat, 'Snow Leopard', getLion);
}

function getLion(cat) {
  return buildFerociousCats(cat, 'Lion', printList);
}

function printList(cat) {
  console.log(cat.list); 
}

Le tout dernier rappel n’enchaîne pas les promesses, car il n’a pas de promesse de retour. Garder une trace des promesses est important pour garantir une continuation à la fin. Par analogie, lorsque nous faisons une promesse, la meilleure façon de tenir la promesse est de se rappeler que nous avons déjà fait cette promesse.

Mettons maintenant à jour l’appel principal avec un appel de fonction thenable :

buildFerociousCats({ list: '', delimiter: commaDelimiter }, 'Panther', getJaguar)
  .then(() => console.log('DONE')); 

Si nous exécutons le code, nous verrons que « DONE » s’imprime à la fin. Si nous oublions de retourner une promesse quelque part dans le flux, « DONE » apparaîtra hors d’usage, car il perd la trace de la promesse faite à l’origine.

N’hésitez pas à consulter le s’engager pour les promesses sur GitHub.

Asynchrone/Attente

Enfin, nous pouvons considérer async/wait comme du sucre syntaxique autour d’une promesse. Pour JavaScript, async/wait est en fait une promesse, mais pour le programmeur, cela ressemble plus à du code synchrone.

À partir du code que nous avons jusqu’à présent, débarrassons-nous du then et enveloppez l’appel autour de async/wait :

async function run() {
  await buildFerociousCats({ list: '', delimiter: pipeDelimiter }, 'Panther', getJaguar)
  console.log('DONE');
}
run().then(() => console.log('DONE DONE')); 

La sortie « DONE » s’exécute juste après la await, car il fonctionne un peu comme du code synchrone. Tant que l’appel à buildFerociousCats renvoie une promesse, nous pouvons attendre l’appel. La async marque la fonction comme renvoyant une promesse, il est donc toujours possible d’enchaîner l’appel dans run avec encore un autre then. Tant que ce que nous appelons en renvoie une promesse, nous pouvons enchaîner les promesses indéfiniment.

Vous pouvez vérifier cela dans le async/wait commit sur GitHub.

Gardez à l’esprit que tout ce code asynchrone s’exécute dans le contexte d’un seul thread. Un rappel JavaScript s’intègre bien dans ce paradigme à thread unique, car le rappel est mis en file d’attente de manière à ne pas bloquer l’exécution ultérieure. Cela permet au moteur JavaScript de suivre plus facilement les rappels et de les récupérer immédiatement sans avoir à gérer la synchronisation de plusieurs threads.

Conclusion

Maîtriser les rappels en JavaScript, c’est comprendre toutes les minuties. J’espère que vous voyez les variations subtiles dans les fonctions JavaScript. Une fonction de rappel devient incomprise lorsque nous manquons des fondamentaux. Une fois que les fonctions JavaScript sont claires, les principes SOLID suivent rapidement. Il faut une solide compréhension des principes fondamentaux pour se lancer dans la programmation SOLID. La flexibilité inhérente au langage place le fardeau de la responsabilité sur le programmeur.

Ce que j’aime le plus, c’est que JavaScript permet une bonne programmation. Une bonne compréhension de toutes les minuties et des fondamentaux nous mènera loin dans n’importe quel Langue. Cette approche des fonctions de rappel est extrêmement importante dans le JavaScript vanille. Par nécessité, tous les coins et recoins feront passer nos compétences au niveau supérieur.




Source link