Ayant été un grand non-sympathisant (pour ne pas dire opposant/rageux) de JavaScript, je n’ai jamais vraiment pris le temps de m’atteler à Node ni les concepts qu’il propose. Du coup, je préviens tout de suite que ces billets risquent de m’être plus utiles qu’à ceux qui les liront, enfonçant des portes ouvertes pour beaucoup d’entre-vous, certainement.
Je me suis donc frotté aux notions d'event loop et d'asynchrone, non sans accrocs.
Double standard
Quand j’ai commencé à m’intéresser aux fondements de Node, donc, la documentation m’a expliqué ceci:
JavaScript execution in Node.js is single threaded, so concurrency refers to the event loop’s capacity to execute JavaScript callback functions after completing other work. Any code that is expected to run in a concurrent manner must allow the event loop to continue running as non-JavaScript operations, like I/O, are occurring.
Ok, mais même si ici on parle bien d’exploiter l'event loop, j’avais toujours un problème pour comprendre pourquoi toutes les opérations d’I/O supportées par Node pouvaient être asynchrones alors que mes opérations (de simples boucles, par exemple) ne l’étaient absolument pas malgré l’utilisation de callback.
Cela pourrait paraître clair pour d’autres, mais la documentation ne faisant référence qu’aux callback et à l’async sans réellement rentrer dans les détails, j’ai fini par croire que les callbacks étaient la cause plutôt que le moyen de faire de l’asynchrone. L’event loop est remplie de callback/task, mais ce n’est pas l’utilisation de callback qui remplit l’event loop.
A queue to rule them all
Après quelques moments passés à chercher ce qui pouvait bien flancher dans mon raisonnement, je suis retombé sur le célèbre service setTimeout
, assez fréquemment utilisé pour commettre les pires atrocités sémantiques que j’aie pu voir dans le développement frontend. Je savais que cela permettait de mettre en arrière-plan des tâches, mais n’étant pas développeur web/js, je m’en suis toujours servi plus ou moins à l’aveugle. Cependant, je me suis attelé à comprendre son fonctionnement (certes basique) pour l’utiliser correctement et aviser en cas d’erreur de ma part.
// Inutile de s'attarder sur les 5000 ms
// ça aurait pu être n'importe quelle valeur
// (0 par exemple).
router.get('/', function(req, res, next) {
const text = new Promise((resolve, reject) => {
setTimeout((message) => {
var i = 0;
while(i < 50000) {
i++;
}
resolve(message);
}, 5000, 'Done!');
})
.then(console.log)
.catch(console.err);
res.send('Hi there!');
});
module.exports = router;
Enfin un résultat positif ! La boucle tourne en arrière-plan et ne bloque plus l’envoi de la page au client ni les opérations adjacentes. Là, pour moi, le fonctionnement est clair. Je sais désormais que, lorsque j’ai affaire à une opération bloquante telle que celle-ci (ou une autre), je suis capable de l’enregistrer dans l’event loop par le biais de setTimeout
et d’en récupérer le renvoi grâce à Promise
. Génial !
Et setImmediate
dans tout ça ?
Une fois la notion assimilée, je suis tombé sur un post de stackoverflow (il me semble) discutant d’un service identique à setTimeout
et setInterval
(dans le fonctionnement): setImmediate()
.
Sa particularité serait de remplacer le commun setTimeout(0)
(i.e. envoi immédiat d’une tâche en arrière-plan) par son appel tout en améliorant les performances et réduisant la consommation d’électricité de cette mise en tâche de fond.
router.get('/', function(req, res, next) {
const heavy_processing = new Promise((resolve, reject) => {
setImmediate((message) => {
var i = 0;
while(i < 50000) {
i++;
}
resolve(message);
}, 'Done!');
})
.then(console.log)
.catch(console.err);
res.send('Hi there!');
});
module.exports = router;
Les modifications sont très minimes, il suffit de supprimer le paramètre milliseconds
de la fonction d’origine pour ne pas avoir de soucis.
Voilà, c’est tout !
Rien d’extraordinaire en soit, mais j’avais vraiment besoin de l’écrire pour garder tout ça en tête. C’était jusqu’ici l’une des seules choses que je ne parvenais pas à saisir et je ne me serais pas senti à l’aise de poursuivre ma découverte sans bien comprendre les fondamentaux. A l’inverse, si un (ou plusieurs) maître jedi JavaScript passe par ici et souhaite corriger certaines inexactitudes, je suis absolument preneur !