Un atelier de Christophe Porteneuve à Paris Web 2016
var christophe = {
age: 38.90896646132786,
family: { wife: 'Élodie', son: 'Maxence' },
city: 'Paris, FR',
company: 'Delicious Insights',
trainings: ['JS Total', 'Node.js', 'Git Total'],
webSince: 1995,
claimsToFame: [
'Prototype.js',
'Ruby On Rails',
'Bien Développer pour le Web 2.0',
'Prototype and Script.aculo.us',
'Paris Web'
]
}
function readPost (req, res) {
Post.findById(req.params.id, function (err, post) {
if (err) {
return handleError(err)
}
post.findRecentComments(function (err, comments) {
if (err) {
return handleError(err)
}
res.render('posts/show', { post, comments, title: post.title })
})
})
}
function normalizeSKUs (skus, cb) {
let normalizations = {}, count, xhrs = []
function handleError (err) {
_(xhrs).invoke('abort')
cb(err)
}
function handleSKU (payload) {
normalizations[payload.from] = payload.to
if (++count < xhrs.length) return
cb(null, normalizations)
}
function normalizeFromServer (sku) {
xhrs.push($.ajax({
url: '/api/v1/skus/normalize',
data: { sku },
onSuccess: handleSKU,
onError: handleError
}))
}
}
Correspondance esprit / code super foireuse
(surtout parce qu’on n’a pas de donnée retournée)
Gestion d’erreur bien bordélique.
Est-ce que ça va tourner ?
Est-ce que ça va bien appeler un seul des callbacks d’erreur ou de succès, et jamais les deux ?
Est-ce que ça va tourner une seule fois ?
Elles sont là depuis longtemps, sous divers noms et formes.
Arrivées en JS en 2007 avec Dōjō.
Promises/A proposé dans CommonJS en 2009 (Kris Zyp)
Promises/A+ en 2012 (Brian Cavalier, Domenic Denicola et al.)
Les APIs web récentes sont basées promesses (ex. fetch, ServiceWorker…)
Des tas de libs (Q, rsvp, bluebird…), polyfills… Désormais natives à ES2015.
La spec de référence. Définit :
then
(chaînage, propagation, etc.)Une promesse ne change d’état qu’une seule fois, d’en attente à accomplie ou rejetée.
Une promesse a une méthode then(…)
Elle prend en fait jusqu’à 2 arguments, tous 2 optionnels :
Notez qu’une API peut s’accomplir sans valeur de résultat (ex. lookup en base de données) : ça ne constitue pas forcément un échec.
lookupUser(userId)
.then(
(user) => console.log('USER:', user.getName()), // Callback de succès
(err) => console.error(err) // Callback d’erreur
)
Mais n’oubliez pas : ils sont exclusifs !
lookupUser(unknownUserId)
.then(
(user) => console.log('USER:', user.getName()), // user === null -> BLAM
(err) => console.error(err) // Pas appelé !
)
Alors on fait comment ?
lookupUser(unknownUserId)
.then((user) => console.log('USER:', user.getName())) // Étape 1
.then(null, (err) => console.error(err)) // Étape 2
Mais then(null, …)
c’est fugly. Alors…
lookupUser(unknownUserId)
.then((user) => console.log('USER:', user.getName())) // Étape 1
.catch((err) => console.error(err)) // Étape 2
Comme pour un try…catch…
, tout callback d’erreur neutralise l’erreur, et on pourrait continuer derrière du coup.
Une étape de la chaîne peut renvoyer une valeur synchrone : elle sera automatiquement convertie en promesse accomplie :
lookupUser(userId)
.then((user) => user.getName()) // Renvoi synchrone de valeur !
.then(console.log) // …et pourtant `.then(…)` :-)
Et une levée d’exception sera convertie en promesse rejetée :
xhrWrap('http://delicious-insights.com')
.then((text) => JSON.parse(text)) // KA-BOOM! (`ParseError`)
.catch(console.error) // Tadaaaaa…
On peut renvoyer une promesse depuis un callback, et celle-ci s’incruste à cet endroit dans la chaîne.
fetch(url)
.then((res) => res.json()) // Promesse de décodage JSON une fois tout le contenu reçu
.then(console.log)
getUser(userId)
.then((user) => user.getFollowers())
.then((followers) => render('user/followers', { followers }))
On risque, à tort, de faire des séquences par réflexe, là où le parallélisme devient possible, puisqu’on est non bloquants…
let tags, entries
getTags()
.then((t) => { tags = t; return getEntries() })
.then((es) => { entries = es; return getEntryCount() })
.then((entryCount) => { render('entries/index', { tags, entries, entryCount }) })
Eh, c’est JS, pas Java/PHP/Ruby/Python les gens !
Promise.all([getTags(), getEntries(), getEntryCount()])
.then(([tags, entries, entryCount]) => {
render('entries/index', { tags, entries, entryCount })
})
Plus rarement, Promise.race
.
Motif du Revealing Constructor :
function promiseMe (itWontHurt) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest()
req.open('GET', url)
req.onload = function () {
if (req.status === 200) {
resolve(req.response)
} else {
reject(new Error(req.statusText))
}
}
req.onerror = function() { reject(new Error('Network error')) }
req.send()
})
}
L’implémentation interne préserve les garanties (transition unique et exclusive, asynchronicité de l’appel des callbacks, etc.)
Tuto génial de Jake Archibald. Enfonce vraiment bien le clou.
promisejs.org, une couverture exhaustive du sujet par Forbes Lindesay.
Promisees, un super bac à sable interactif de Nicolás Bevacqua.
$ npm i -g promise-it-wont-hurt
$ promise-it-wont-hurt
Fiable : seulement un des callbacks, une seule fois, garanti. Et la gestion des exceptions en prime !
Composable : chainable, avec injection et conversion.
Nouvelles abstractions
Légère pénalité de performance (va disparaître)
Le flux du code reste non-intuitif / très différent du code synchrone.
Adapté à des traitements one-shot : pas pour du récurrent / multiple / indéfini (type flux, événements, etc.). On préfèrera alors des observables, comme avec RxJS ou BaconJS, par exemple.
Le TC39 bosse sur le fait de rendre les promesses annulables, ce qui est critique pour tout un tas de cas. Facilite aussi le fait de les soumettre à un timeout, qui aujourd’hui nécessite un peu un hack :
const timeoutPromise = Promise.race([
fetch(someSlowURL), timeout(3000)
])
function timeout (delay) {
return new Promise((_, reject) => { setTimeout(reject, delay, new Error('timeout!')) })
}
C’est un peu le Saint Graal du JS asynchrone.
À peu près copié-collé de C# 5.0 / .NET 4.5 (2012).
(Oui, y’a des choses bien dans les langages de chez MS ; merci Anders)
async function loadStory () {
try {
const story = await getJSON('story.json')
addHtmlToPage(story.heading)
for (let chapterPromise of story.chapterUrls.map(getJSON)) {
const chapter = await chapterPromise
addHtmlToPage(chapter.html)
}
addTextToPage('All done')
} catch (err) {
addTextToPage(`Argh, broken: ${err.message}`)
}
hideSpinner()
})
async function login (username, password, session) {
const user = await getUser(username)
const hash = await crypto.hashAsync(password + user.salt)
if (user.hash !== hash) {
throw new Error('Incorrect password')
}
session.setUser(user)
})
async function loadDashboard (req, res) {
const user = await loadUser(req.session.userId)
const [profile, notifs] = await Promise.all([
user.getProfile(), user.getNotifications()
])
res.render('dashboard', { user, profile, notifs })
}
La vie elle est trop beeeellllleeee ! ❤️
Comme toutes les nouvelles fonctionnalités (ex. promesses, générateurs, proxies…) il faut encore optimiser la performance. Mais ça vaut trop le coup. Et ça sera performant. Regardez la tendance de JS en général.
Sera officiellement dans ES2017. Finalisé en juillet dernier.
Déjà dispo en natif dans Edge 13+ et Chrome 53+… derrière un flag.
Babel est ton ami (polyfille à coup de promesses + générateurs).
Christophe Porteneuve
Les slides sont sur bit.ly/pw16-async