Skip to content

Instantly share code, notes, and snippets.

@jgrenat
Created June 14, 2016 12:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jgrenat/757b3b8f2a2e54eb61e835c69b6b66fc to your computer and use it in GitHub Desktop.
Save jgrenat/757b3b8f2a2e54eb61e835c69b6b66fc to your computer and use it in GitHub Desktop.
devoxx-write-up
# Oubliez les migraines, faites de l'asynchrone... synchrone !
Après plus de six ans sans améliorations au langage JavaScript, la nouvelle version EcmaScript 2015 a apporté un vent de fraîcheur pour le développeur. Mûrement réfléchie et résolument orientée vers la *Developer Experience*, elle offre une toute nouvelle gamme d'outils adaptés à des problématiques récurrentes de la vie du développeur.
Lors de [Devoxx France 2016](http://www.devoxx.fr/), j'ai [présenté un live-coding](http://cfp.devoxx.fr/2016/talk/OFN-2448/Oubliez_les_migraines,_faites_de_l'asynchrone..._synchrone_!) se concentrant sur les nouveaux outils permettant une meilleure gestion de l'asynchrone – une composante essentielles du développement JavaScript. Cet article est une adaptation de cette présentation.
## L'asynchrone via les callbacks
Traditionnellement, l'asynchrone a été géré au travers de fonctions de callback – des fonctions transmises à des fonctions asynchrones afin d'être appelées une fois la tâche asynchrone terminée. Ainsi, pour récupérer par exemple les informations d'un utilisateur (via un back-end ou autre) et afficher son nickname, on peut obtenir le code suivant :
```js
getUserDataAsynchronously((errors, user) => {
if (errors) {
console.error('Something bad happened...');
} else {
console.log(`User's nickname ${user.nickname}.`);
}
});
```
Cette méthode fonctionne parfaitement mais présente plusieurs désavantages. Le premier concerne les cas où on va devoir effectuer plusieurs appels à la suite, chacun dépendant du précédent. On se retrouve rapidement à devoir indenter exagérément son code. Un exemple est cependant plus parlant :
```js
authenticateUser((err, userData) => {
if (err) {
console.error(err);
return;
}
getUserInfos(userData.id, (err, userInfos) => {
if (err) {
console.error(err);
return;
}
getUserFriends(userInfos.friendsIds, (err, friends) => {
if (err) {
console.error(err);
} else {
console.log('We finally have our friend\'s informations!');
}
});
});
});
```
Ici, on peut repérer immédiatement la succession d'accolade fermantes en fin de code, qui est le symptôme le plus parlant de ce *callback hell* comme l'appellent les développeurs ! L'autre problème est également bien visible dans le code ci-dessus : la gestion des erreurs ne semble pas naturelle et nous force à interrompre manuellement notre *workflow* à coup de `return` ou de conditions.
Alors comment remédier à ça ? Un premier outil qui va nous aider et autour duquel s'articulent les autres outils est l'API Promise.
## Les *promises* et leurs promesses
Une promise – promesse en français – est un objet qui peut avoir trois états différents :
- En attente de résolution
- Résolu avec succès
- Résolu en échec
Sur cet objet, on peut utiliser deux méthodes. La première, la méthode `then` permet d'exécuter une fonction en cas de succès tandis que la seconde, la méthode `catch`, permet d'exécuter une fonction en cas d'échec. On peut également fournir un second argument à `then` pour les erreurs. Si l'on considère que la fonction `getUserDataAsynchronously` retourne une promise, notre premier exemple devient ainsi :
```js
getUserDataAsynchronously()
.then(user => console.log(`User's nickname ${user.nickname}.`))
.catch(errors => console.error('Something bad happened...'));
});
```
Le code est un peu plus clair et on a une gestion d'erreur bien séparée. Le principal avant des promesses est cependant la possibilité de les chaîner. En effet, ces deux méthodes qu'on vient de voir retournent elles-même des promises, mais pas la même que la promise initiale !
Si la fonction fournie retourne une valeur, cette valeur est transmise au prochain `then`. Si elle retourne une promise, le prochain `then` sera appelé une fois cette promise résolue et avec la valeur de cette promise. Si elle retourne une erreur, l'erreur sera transmise au prochain `catch` ou au prochain `then` ayant un second paramètre. Voici ce que cela peut donner sur notre second exemple de tout à l'heure :
```js
authenticateUser()
.then(userData => getUserInfos(userData.id))
.then(userInfos => getUserFriends(userInfos.friendsIds))
.then(friends => {
console.log('We now have our friend\'s informations!');
})
.catch(errors => console.error(errors));
```
Comme vous pouvez le voir, le code est à la fois bien plus concis et bien plus lisible ! De plus, la gestion d'erreur peut se faire à un seul et même endroit, ce qui a du sens puisqu'il s'agit d'un extrait de code destiné à un travail précis.
Imaginons maintenant qu'on ait des appels asynchrones qui doivent être exécutés en parallèle puis aggrégés. Partons par exemple du principe que nous n'avons pas un seul appel à faire pour récupérer les informations des amis de l'utilisateur, mais deux fonctions différentes, une pour récupérer les amis favoris et un autre pour récupérer les autres amis :
```js
authenticateUser()
.then(userData => getUserInfos(userData.id))
.then(userInfos => {
getUserFavoriteFriends(userInfos.favoriteFriendsIds));
getUserOtherFriends(userInfos.otherFriendsIds));
})
.then(friends => {
console.log('We now have our friend\'s informations!');
})
.catch(errors => console.error(errors));
```
Ce code ne peut pas marcher puisqu'on ne retourne rien dans le second `then`. Par conséquent dans le troisième `then`, `friends` vaut `undefined`. Ce qu'il nous faut, c'est donc un moyen de retourner une promise qui sera résolue une fois que nos deux promises seront résolues. C'est ce que fait `Promise.all`, voici comment l'utiliser :
```js
authenticateUser()
.then(userData => getUserInfos(userData.id))
.then(userInfos => {
const favoritesPromise = getUserFavoriteFriends(userInfos.favoriteFriendsIds));
const othersPromise = getUserOtherFriends(userInfos.otherFriendsIds));
return Promise.all([favoritesPromise, othersPromise]);
})
.then(([favorites, others]) => {
console.log('We now have our friend\'s informations!');
})
.catch(errors => console.error(errors));
```
On résout ainsi facilement un problème qui serait autrement plus compliqué à résoudre avec des callbacks (essayez, vous verrez qu'il est impossible d'avoir un exemple aussi concis !)
L'API Promise possède quelques autres méthodes très utiles :
- `Promise.race` : Prend un tableau de Promise en entrée et retourne une promise résolue dès qu'une des promises est résolue avec la valeur résolue de cette promise ;
- `Promise.resolve` : Retourne une promise résolue avec succès avec la valeur fournie en argument ;
- `Promise.reject` : Retourne une promise résolue en erreur avec la valeur fournie en argument.
## Allons plus loin avec les *generators*
Ce code est déjà plus joli, mais on peut aller plus loin ! Et si on pouvait faire de l'asynchrone... synchrone ? C'est possible grâce aux *generators*, un nouveau concept apporté par ES 2015.
### Mais c'est quoi un *generator* ?
Une *generator* est une fonction capable de se mettre en pause au cours de son exécution jusqu'à ce qu'on l'appelle de nouveau. Encore une fois, un exemple sera plus parlant. Dans cet extrait de code, on crée un itérateur sur notre *generator* puis on l'exécute. A chaque fois que celui-ci rencontre le mot-clé `yield`, il retourne la valeur qui le suit à celui qui consomme le *generator* :
```js
function *myGenerator() {
yield 1;
yield 'Bonjour';
}
const iterator = myGenerator();
it.next().value; // 1
it.next().value; // 'Bonjour'
it.next().value; // undefined
```
Mais cette communication *consumer* / *generator* marche dans les deux sens. En appelant `it.next()`, on peut fournir une valeur qui sera récupérable dans le *generator* :
```js
function *myGenerator() {
const value = yield 1;
yield 'Bonjour';
yield value;
}
const it = myGenerator();
it.next().value; // 1
it.next('TestValue').value; // 'Bonjour'
it.next().value; // 'TestValue'
```
On peut également envoyer une erreur au *generator* :
```js
function *myGenerator() {
try {
yield 1;
} catch(err) {
yield `Error: ${err}`;
}
yield 'Bonjour';
}
const it = myGenerator();
it.next().value; // 1
it.throw('SomeError').value; // 'Error: SomeError'
it.next().value; // 'Bonjour'
```
### Et... ça m'avance en quoi ?
En effet, cela ne semble pas servir à grand chose dans notre cas. Pourtant, songez à ce qu'il peut se passer dès qu'on commence à combiner les *generators* et les *promises*. Imaginons qu'au lieu de renvoyer des valeurs quelconques, le *generator* nous retourne des *promises*. On pourrait alors attendre que la promise soit résolue pour appeler de nouveau le générateur et lui renvoyer la valeur attendue. Voilà à quoi ça ressemble en reprenant notre tout premier exemple :
```js
function *myGenerator() {
try {
const userData = yield getUserDataAsynchronously();
console.log(`User's nickname ${user.nickname}.`);
} catch(error) {
console.error('Something bad happened...');
}
}
const it = myGenerator();
const promise = it.next().value;
promise
.then(value => it.next(value))
.catch(error => it.throw(error));
```
Ce qui est vraiment intéressant ici, c'est que le code a l'intérieur du *generator* ressemble à du code synchrone alors qu'il est en fait totalement asynchrone ! On peut même gérer les erreurs avec un `try...catch`, ce qui ne fonctionne pas avec les *callbacks* ou les *promises* !
Imaginons maintenant une fonction qui effectue ce méchanisme exact, à savoir itérer sur notre *generator*, et à chaque fois qu'on reçoit une *promise*, attendre que celle-ci soit résolue (en échec ou en erreur) pour exécuter notre *generator*. Eh bien on pourrait simplement placer notre code dans des *generators* et développer comme si exécutait du code synchrone ! Ce procédé s'appelle des coroutines, et on peut utiliser notamment le module [`co`](https://www.npmjs.com/package/co) pour cela. Reprenons notre second exemple en utilisant cette bibliothèque :
```js
co(function*() {
const userData = yield authenticateUser();
const userInfos = yield getUserInfos(userData.id);
const favoritesPromise = getUserFavoriteFriends(userInfos.favoriteFriendsIds));
const othersPromise = getUserOtherFriends(userInfos.otherFriendsIds));
yield Promise.all([favoritesPromise, othersPromise]);
});
```
Et voilà, notre code a maintenant l'air totalement synchrone ! Notons tout de même que notre gestion d'erreur a disparu. Nous pourrions – comme dans l'exemple ci-dessus – utiliser un `try...catch`, mais on peut aussi profiter du fait que `co` retourne une *promise* :
```js
co(function*() {
const userData = yield authenticateUser();
const userInfos = yield getUserInfos(userData.id);
const favoritesPromise = getUserFavoriteFriends(userInfos.favoriteFriendsIds));
const othersPromise = getUserOtherFriends(userInfos.otherFriendsIds));
yield Promise.all([favoritesPromise, othersPromise]);
}).catch(err => console.log(err));
```
## Place aux *async* functions
Cette syntaxe est tellement pratique qu'elle sera probablement standardisée dans une prochaine version de la spécification. La [proposition](http://tc39.github.io/ecmascript-asyncawait/) est actuellement au stade 3 et sera sûrement présente dans ECMAScript 2017. Avec cette nouveauté, notre dernier exemple deviendrait :
```js
async function myAsyncFunction() {
const userData = await authenticateUser();
const userInfos = await getUserInfos(userData.id);
const favoritesPromise = getUserFavoriteFriends(userInfos.favoriteFriendsIds));
const othersPromise = getUserOtherFriends(userInfos.otherFriendsIds));
await Promise.all([favoritesPromise, othersPromise]);
}
myAsyncFunction().catch(err => console.log(err));
```
## Soyez plus réactifs
Nous avons vu ainsi comment organiser bien mieux son code asynchrone lorsqu'on attend une valeur en retour. Mais que faire lorsque notre évènement asynchrone est en réalité... une suite d'évènements asynchrones ?
On peut citer ainsi l'écoute des évènements utilisateurs. Imaginons qu'on souhaite effectuer une action à chaque clic sur un bouton. Voici comment on fait cela avec les callbacks :
```js
const element = document.getElementById('myButton');
element.addEventListener('click', function(event) {
console.log('Button clicked!', event);
}), false);
```
Depuis quelques temps émerge en JavaScript un nouveau paradigme : la programmation réactive, reposant notamment sur les *Observable*. A ce jour, la spécification pour la standardisation des *Observable* n'en est qu'au stade 1, mais il existe des bibliothèques comme [RxJS](https://github.com/Reactive-Extensions/RxJS) utilisables dès aujourd'hui. Voici ce que donnerait ainsi l'exemple précédent :
```js
const element = document.getElementById('myButton');
const source = Observable.fromEvent(element, 'click');
source.subscribe(event => {
console.log('Button clicked!', event);
});
```
L'idée est de ne plus traiter un évènement seul, mais plutôt un flux – *stream* – d'évènements auquel on peut s'abonner. Cette description est très réductrice et rend peu honneur à la programmation réactive, mais cette notion nécessiterait un article entier pour être développée. Je vous conseille donc de [lire cette excellente introduction](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) de [André Medeiros](http://andre.staltz.com).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment