async / await deep-dive
Une présentation de Christophe Porteneuve at Node.js Paris
Une présentation de Christophe Porteneuve at Node.js Paris
const christophe = {
age: 39.49007529089665,
family: { wife: 'Élodie', son: 'Maxence' },
city: 'Paris, FR',
company: 'Delicious Insights',
trainings: ['JS Total', 'Node.js', 'Git Total'],
upcoming: ['ES2015+', 'GitHub', 'Performance Web'],
webSince: 1995,
claimsToFame: [
'Prototype.js',
'Ruby On Rails',
'Prototype and Script.aculo.us',
'Paris Web',
'NodeSchool Paris'
]
}
router.get('/ohai', (req, res) => {
res.json({ result: 'success' })
})
$(document).on('click', 'a.js-toggle', (event) => {
$(event.currentTarget).toggle()
})
router.get('/posts/:id', (req, res) => {
db.posts.find(req.params.id, (err, post) => {
if (err) return handleError(err)
post.author.find((err, author) => {
if (err) return handleError(err)
post.comments.find({ limit: 10 }, (err, comments) => {
if (err) return handleError(err)
res.render('posts/show', { post, author, comments })
})
})
})
})
const { waterfall } = require('async')
router.get('/posts/:id', (req, res) => {
let post, author
waterfall([
(cb) => db.posts.find(req.params.id, cb),
(p, cb) => (post = p).author.find(cb),
(a, cb) => (author = a).comments.find({ limit: 10 }, cb),
(comments) =>
res.render('posts/show', { post, author, comments })
], handleError)
})
ReferenceError: lookMaNoMethod is not defined
at Timeout.setTimeout (repl:1:18)
at ontimeout (timers.js:380:14)
at tryOnTimeout (timers.js:244:5)
at Timer.listOnTimeout (timers.js:214:5)
router.get('/posts/:id', (req, res) => {
db.posts.find(req.params.id, (err, post) => {
post.author.find((err, author) => {
post.comments.find({ limit: 10 }, (err, comments) => {
res.render('posts/show', { post, author, comments })
})
})
})
})
mongoose.on('open', () => {
// If Mongoose was already connected, tough luck.
})
return
manquant
function badActor (auth, cb) {
verifyCredentials(auth, (err, info) => {
if (err) {
cb(err) // Oops, no return!…
}
persistAccess(info)
// …so let's mess it up!
cb(null, { token: info.token })
})
}
function zalgoFetcher (key, cb) {
if (cache.hasOwnProperty(key)) {
// Synchronous call
return cb(null, cache[key])
}
fetchFromAPI(key, (err, data) {
if (err) {
return cb(err);
}
cache[key] = data
// Asynchronous call
cb(null, data)
})
}
Pas juste une syntaxe alternative, une « mise à plat »
Promesses = garanties contre des anti-patterns
Qui plus est, facilement composable
Devient vite lisible à l’usage mais…
…sera toujours moins lisible que du bon vieux code bloquant : conditions, boucles, try…catch
…
Ce n’est pas : callbacks ➡️ promesses ➡️ async
/await
C’est intimement lié aux promesses
Ça ne les remplace pas
Fait partie (depuis juillet 2016) du dernier standard officiel
Syntaxe déjà prise en charge nativement
v8 5.5, donc Cr55, Op42, Saf 10.1, Node 7.6
Par ailleurs, Fx52 (mars 2017), Edge build 14986 (14/12/2016)
…
Clairement le futur
(mais maintenant)
Fonctions classiques :
async function foo (itemId) {
const details = await fetchItem(itemId)
doInternalWork(details)
return details
}
Ou fonctions fléchées :
connection.on('open', async () => {
await syncNoSQL(connection)
infoLog('DB ready, NoSQL sync’ed.')
})
Une fonction async renvoie implicitement une promesse : on peut la consommer tant en « gelé » avec un await
qu’en « futur » avec un bon vieux .then
.
const result = await foo()
// …but could also be…
foo().then((result) => …)
Du coup ça marche partout où les promesses marchent (harnais de test, libs d’assertions : Mocha, Jest, Chai-as-Promised…)
it('should allow status pings', async () => {
const { result } = await apiCall('/ohai')
expect(result).to.equal('success')
})
Attention à ne pas se faire avoir par les callbacks internes discrets
async function foo () {
getRootId((id) => {
// Current function is the callback: no `async`,
// so no `await`!
const item = await fetchItem(id)
processItem(item)
})
}
On await
toujours une promesse
(ce qui inclue, du coup, tout appel de fonction async
)
Si on await
sur quoi que ce soit d’autre, ça enrobe simplement via un classique Promise.resolve
, tout comme au sein d’une chaîne de promesses :
async function foo () {
// will turn into a `Promise.resolve(42)`
return await 42
}
async function showUser (req, res) {
try {
const user = await User.find(req.params.id)
res.render('users/show', { user })
} catch (err) {
handleError(err)
}
}
async function showPost (req, res) {
try {
const post = await db.posts.find(req.params.id)
const author = await post.author.find()
const comments = await post.comments.find({ limit: 10 })
res.render('posts/show', { post, author, comments })
} catch (err) {
handleError(err)
}
}
async function showPost (req, res) {
try {
const post = await db.posts.find(req.params.id)
const [author, comments] = await Promise.all([
post.author.find(),
post.comments.find({ limit: 10 })
])
res.render('posts/show', { post, author, comments })
} catch (err) {
handleError(err)
}
}
const { delay, limit, map } = require('awaiting')
async function superDuper (req, res) {
try {
// Simulate load
await delay(500)
// Promise timeout!
const user = await limit(User.find(req.params.id), 2000)
// Capped parallel async mapping
const payloads = await map(req.params.urls, 3, fetchText)
await user.processPayloads(payloads)
res.json({ result: 'success' })
} catch (err) {
handleError(err)
}
}
Le risque, c’est de reproduire à tort les séquences 100% bloquantes des langages traditionnels, sans réfléchir :
async function listEntries (req, res) {
try {
// Here goes stupid…
const entries = await Entry.getEntries(req.query)
const entryCount = await Entry.count().exec()
const tags = await Entry.tags()
res.render('entries/index', { entries, entryCount, tags })
} catch ({ message }) {
req.flash('error',
`Les bookmarks n’ont pu être affichés (${message})`)
res.redirect('/')
}
}
Paralléliser ce qui peut l’être
async function listEntries (req, res) {
try {
// Aaaah, much better.
const [entries, entryCount, tags] = await Promise.all([
Entry.getEntries(req.query),
Entry.count().exec(),
Entry.tags()
])
res.render('entries/index', { entries, entryCount, tags })
} catch ({ message }) {
req.flash('error',
`Les bookmarks n’ont pu être affichés (${message})`)
res.redirect('/')
}
}
return await
async function apiCall({ path, method, payload }) {
// This `await` is useless and could "hurt" performance…
return await fetch(`/api/v1/${path}`, {
method,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
})
}
Aucun intérêt, puisque le résultat est de toutes façons déjà wrappé…
paths.forEach(
async (path) => console.log((await fsStat(path)).size)
)
Ce sont des promesses :
elles sont ici parallèles, non ordonnées et non attendues.
for-of-await
: séquentiel
(async () => {
for (const fileName of fileNames) {
console.log((await fsStat(fileName)).size)
}
})()
C’est un peu mieux (au moins, on attend la fin, et l’ordre est préservé), mais inutilement séquentiel : chaque appel n’a pas besoin des autres, et l’ordre interne des appels à stat
est sans importance.
await-all-map
: parallèle
(async () => {
const sizes = await Promise.all(fileNames.map(
(fileName) => fsStat(fileName).then(({ size }) => size)
))
sizes.forEach((size) => console.log(size))
})()
À préférer chaque fois qu’on peut paralléliser les traitements.
≈ 60% meilleur en natif que la transpilation à base de générateurs
Babel : utilisez latest-minimal
(Node) ou env
(iso), pas latest
, qui transpilera tout en ES5 et passera donc par regenerator etc.
Babel + nodent : ajoutez fast-async pour ≈ 30% de boost.
Les mêmes que les promesses : c’est du one-shot
Si vous voulez du multi-déclenché,
utilisez des event emitters ou des observables.
Async iterations / iterators (for-await
, stade 3, ES2018 « sûr »)
for await (const line of readLines(filePath)) {
console.log(line)
}
Observables natifs (stade 1, très prochainement stade 2, sans doute ES2019). RxJS suit de près pour être interopérable.
function commandKeys (element) {
const keyCommands = { '38': 'up', '40': 'down' }
return listen(element, 'keydown')
.filter((event) => event.keyCode in keyCommands)
.map((event) => keyCommands[event.keyCode])
}
Christophe Porteneuve
Les slides sont sur bit.ly/deep-dive-async-await