[Article] Découvrez le yielded style programming ou développement JavaScript asynchrone linéaire
Découvrez le développement JavaScript asynchrone linéaire, avec yld
Si vous avez déjà fait du JavaScript asynchrone, vous avez certainement connaissance de techniques de traitement de la réponse.
Les callbacks
Ils vous permettent de traiter une réponse après l'exécution d'une fonction asynchrone, en cas d'erreur ou non.
Ils ont pour défauts de devoir avoir des fonctions gérant l'échec ou la réussite et de pousser vers un développement plein de fonctions imbriquées, nuisant à sa lisibilité et l'espace des arguments de votre fonction est pollué par les callbacks.
Pour palier cela, une autre méthode est apparue, les promesses (promises, en anglais).
Les promises
Les promesses ont une structure permettant aussi de gérer l'échec ou la réussite de l'exécution d'une fonction, d'exécuter des fonctions pendant la progression de l'exécution, etc.
Elles se différencient par le fait qu'une promesse est un objet auquel on passe tout un tas de fonctions, via ses méthodes, afin de gérer les différents états de l'exécution.
On se retrouve donc avec énormément de fonctions dans son code avec, parfois, des références internes.
Cela a tendance à déstructurer votre code de telle sorte que sa relecture peut devenir une vraie gymnastique cérébrale.
Le mot-clé yield
Me tenant à jour concernant les avancées du JavaScript, j'ai découvert, parmi les propositions de la future norme ECMAScript 6, le mot-clé yield.
Ce mot-clé, une sorte de return particulier, permet de créer des fonctions retournant un générateur, comprenez par là que la liste d'instructions que contient votre fonction fera un arrêt à chaque yield qu'elle contient, jusqu'à ce que vous appeliez le yield suivant, via la méthode next().
Exemple :
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13
| var generate, generator;
generate = function generate(value) {
yield 'Hello ' + value;
};
generator = generate('World');
// on appelle le yield suivant du générateur, après 2 secondes
setTimeout(function () {
// renverra "Hello World" dans votre console, après les 2 secondes
console.log(generator.next());
}, 2000); |
Ces générateurs ont aussi une une méthode send().
Celle-ci permet d'envoyer une liste de valeurs au générateur, à un moment donné pendant le parcours des yield qu'il contient.
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| var generate, generator;
generate = function generate() {
var message, response;
message = 'Hello ';
response = yield message;
yield message + response;
};
generator = generate();
// on démarre le générateur, affichera "Hello "
console.log(generator.next());
// on appelle le yield suivant du générateur, après 2 secondes
setTimeout(function () {
// renverra "Hello World" dans votre console, après les 2 secondes
console.log(generator.send('World'));
}, 2000); |
Enfin, sachez que les générateurs ont aussi une méthode close(), afin de libérer la mémoire.
Le yielded style programming
Partant de cette découverte, je me suis dit qu'il devrait être possible d'affecter à une variable du contexte (scope) courant, le résultat d'une fonction asynchrone et d'ensuite poursuivre le processus.
C'est ainsi qu'est né yld (prononcez yielded).
Il s'agit d'un outil vous permettant de transformer un générateur en une liste d'instructions s'exécutant l'une après l'autre, comme s'il s'agissait d'une simple fonction mais attendant la réponse de fonctions asynchrones, quand c'est nécessaire.
De plus, il ajoute une notion de relation entre les différents scopes.
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| var asyncFn1, asyncFn2;
asyncFn1 = yld(function (a, b) {
var child, response;
if (isNaN(a) || isNaN(b)) {
// stoppe le processus immédiatement et renvoie une erreur avec le message spécifié
this.error = 'Invalid values';
}
// this.yld est utilisable comme yld
child = yield this.yld(asyncFn2)(a);
response = yield child.send(b);
console.log(response); // 3
});
asyncFn2 = function (a) {
var b, parent;
parent = this.parent;
// retourne le scope courant au scope parent
b = yield parent.send(this);
// retourne la réponse au scope parent
yield setTimeout(function () {
parent.send(a + b);
}, 3000);
};
asyncFn1(1, 2); |
Comme vous pouvez le constater, il n'y a que très peu de fonctions, vous passez uniquement les arguments dont vos fonctions ont besoin et, surtout, le processus s'interrompt à chaque yield, vous permettant de récupérer une valeur sur la même ligne que l'appel à une fonction asynchrone, comme si elle ne l'était pas.
N.B. : Le mot-clé yield étant une possibilité d'amélioration future du JavaScript, il est possible que yld ne s'exécute pas encore partout, néanmoins, yld est déjà conçu pour pouvoir s'exécuter en navigateur et sous Node.js.
Pour tester les différents exemples, je vous recommande l'utilisation d'un Firefox à jour.
EDIT : Le plus facile, pour vos tests, c'est via la console de Firebug.
Sinon, vous pouvez l'embarquer, dans votre HTML via
Code:
<script type="application/javascript;version=1.7"></script>
Où le 1.7 est, évidemment, la version minimale.
Source : https://github.com/Lcfvs/yld
:fleche: Malgré l'habitude évidente qu'il vous faudra pour complètement en tirer avantage, trouvez-vous que cela peut réellement améliorer la lisibilité de votre code?
:fleche: Sinon, qu'y reprochez-vous?
Comparaison des différentes méthodes
@ SylvainPV :
Merci pour ce premier avis, tu trouveras donc les exemples demandés ci-dessous.
Le callback :
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
var handler1, handler2, handler3;
handler1 = function handler1(value, callback) {
var error, newValue;
if (isNaN(value)) {
error = new Error('value isn\'t a number');
} else {
newValue = value + 1;
}
callback(error, newValue, handler3);
};
handler2 = function handler2(error, value, callback) {
var newError, newValue;
if (error) {
newError = error;
} else if (parseInt(value) !== value) {
error = new Error('value isn\'t an integer');
} else {
newValue = value + 1;
}
callback(newError, newValue);
};
handler3 = function handler3(error, value) {
if (error) {
throw error;
} else {
console.log(value);
}
};
handler1(1, handler2); |
Comme on peut le voir, on pollue les arguments avec une éventuelle erreur, que l'on doit à chaque fois tester, soi-même, dans le callback.
La promise :
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
var defer, handler1, errorHandler1, handler2, errorHandler2, handler3, promise;
defer = require("promise").defer;
handler1 = function handler1(value) {
var deferred;
deferred = defer();
if (isNaN(value)) {
deferred.reject();
} else {
deferred.resolve(value + 1);
}
return deferred.promise;
};
errorHandler1 = function errorHandler1(error) {
throw new Error('value isn\'t a number');
};
handler2 = function handler2(value) {
var deferred;
deferred = defer();
if (parseInt(value) !== value) {
deferred.reject();
} else {
deferred.resolve(value + 1);
}
return deferred.promise;
};
errorHandler2 = function errorHandler2(error) {
throw new Error('value isn\'t an integer');
};
handler3 = function handler3(value) {
console.log(value);
};
promise = handler1(1);
promise.then(handler2, errorHandler1)
.then(handler3, errorHandler2); |
Les arguments sont plus propres, mais on remarque la création de beaucoup plus de fonctions, dans son code.
De plus, vous devez, vous-même, gérer le defer et retourner la promise.
La yielded :
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
var handler1, handler2, handler3, yielded;
handler1 = function handler1(value, handler2, handler3) {
var child, response;
if (isNaN(value)) {
this.error = 'value isn\'t a number';
}
child = yield this.yld(handler2)(value + 1);
response = yield child.send();
yield handler3(response);
};
handler2 = function handler2(value) {
var parent;
if (parseInt(value) !== value) {
this.error = 'value isn\'t an integer';
}
parent = this.parent;
yield parent.send(this);
yield parent.send(value + 1);
};
handler3 = function handler3(value) {
console.log(value);
};
yielded = yld(handler1);
yielded(1, handler2, handler3); |
Comme on peut le voir, on déclare aussi peu de fonctions qu'avec la méthode du callback, le premier handler gère chacune des réponses, lui-même, et une relation existe entre les différents handlers, permettant au processus enfant de communiquer avec le processus parent et inversement.
Pour comprendre ce que fait le code, il suffit donc simplement d'observer ce qui se passe dans le premier handler.
Enfin, contrairement aux promises, il devient très aisé, pour le premier handler (dans l'exemple), de savoir quand chacun des autres processus finit.