theme | titleTemplate | background | download | exportFilename | class | highlighter | lineNumbers | favicon | info | drawings | css | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
./theme |
L’asynchrone en JS sans le cringe |
/jeshoots-com--2vD8lIhdnw-unsplash.jpg |
true |
asynchrone-en-js-sans-le-cringe |
text-center |
shiki |
true |
## L’asynchrone en JS sans le cringe
Une présentation de Christophe Porteneuve
Envie de plus ? Notre [chaîne YouTube](https://www.youtube.com/c/DeliciousInsights) et nos [super formations](https://delicious-insights.com/fr/formations/) sont pour toi !
|
|
unocss |
Une présentation de Christophe Porteneuve
const christophe = {
family: { wife: 'Élodie', sons: ['Maxence', 'Elliott'] },
city: 'Paris, FR',
company: 'Delicious Insights',
trainings: ['TypeScript', 'React PWA', 'Node.js', 'ES Total'],
jsSince: 1995,
claimsToFame: [
'Prototype.js',
'script.aculo.us',
'Bien Développer pour le Web 2.0',
'NodeSchool Paris',
'Paris Web',
'dotJS'
]
}
export async function getAllRoles(req, res) {
res.send({ data: ROLES })
}
export async function getAllRolesWithAbilities(req, res) {
const data = computeCombinedAbilitiesByRole(Object.keys(ROLES))
res.send({ data })
}
async function create(createData) {
return GeneralParameter.create(createData)
}
export function getAllRoles(req, res) {
res.send({ data: ROLES })
}
export function getAllRolesWithAbilities(req, res) {
const data = computeCombinedAbilitiesByRole(Object.keys(ROLES))
res.send({ data })
}
function create(createData) {
return GeneralParameter.create(createData)
}
const mailSequence = mails.map(async (mail) => await sendMail(mail))
const mailSequence = mails.map(async (mail) => await sendMail(mail))
const mailSequence = mails.map((mail) => sendMail(mail))
(Éventuellement, si tu peux garantir que sendMail
n’utilise que son premier argument, et ne sera donc pas gêné par des arguments supplémentaires :)
const mailSequence = mails.map(sendMail)
const mailSequence = mails.map(async (mail) => await sendMail(mail))
Parallélisé (court-circuit sur 1ère erreur temporelle) :
const mailSequence = await Promise.all(mails.map((mail) => sendMail(mail)))
Séquencé (court-circuit sur première erreur itérative) :
const mailSequence = []
for (const mail of mails) {
mailSequence.push(await sendMail(mail))
}
async function getUserById(id) {
const user = await User.findByPk(…)
return user
}
async function upsertSetting(formObject) {
// …
return noRecord ? await create(fields) : await update(fields)
}
async function renewToken({ commit }) {
const { token, refreshToken } = await renewToken()
const setToken = await commit('setToken', { token, refreshToken })
return setToken
}
function getUserById(id) {
return User.findByPk(…)
}
function upsertSetting(formObject) {
// …
return noRecord ? create(fields) : update(fields)
}
async function renewToken({ commit }) {
const { token, refreshToken } = await renewToken()
return commit('setToken', { token, refreshToken })
}
Si on transforme le résultat (par exemple en ne renvoyant qu'une partie), on doit forcément faire un await
local pour ensuite transformer avant de renvoyer :
async function logIn(req, res) {
const { token } = await attemptLogIn(req.body)
return token
}
async function process(items) {
try {
…
return await subProcess(items)
} catch (error) {
console.error(`Couldn't run subprocess for ${items}: ${error}`)
throw error // Or possibly provide a fallback value, or something.
}
}
Si on peut traiter localement l'erreur que la promesse est susceptible de lever, il faut un await
pour que celle-ci soit levée au sein du try…catch
.
Une parallélisation n'est pas toujours préférable, mais quand elle l'est, séquencer « par défaut » laisse de la performance sur la table.
async function bulkCreateOrUpdate(data) {
…
for (let i = 0; i < data.length; i++) {
await User.upsert(data[i], { transaction })
}
…
}
Cadeau bonus : ça permet dans ce cas précis de virer cette fichue boucle numérique qui aurait dû être une jolie for
…of
. Y'avait rien qu'allait dans ce code.
async function bulkCreateOrUpdate(data) {
…
await Promise.all(data.map((userData) => User.upsert(userData, { transaction })))
…
}
Et si on est limités dans la parallélisation (ex. connexions à la base de données), pas de souci, on a des solutions pour plafonner :
import { map as cappedAll } from 'awaiting'
await cappedAll(data, 5, (userData) => User.upsert(userData, { transaction }))
Non mais 🤮, quoi.
async function down(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction()
try {
await queryInterface
.bulkDelete('user_org_roles', null, {})
.then(() => queryInterface.bulkDelete('user', null, {}))
.then(() => queryInterface.bulkDelete('person', null, {}))
await transaction.commit()
} catch (error) {
await transaction.rollback()
throw error
}
}
Utilise juste async
/ await
, enfin !
async function down(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction()
try {
await queryInterface.bulkDelete('user_org_roles')
await queryInterface.bulkDelete('user')
await queryInterface.bulkDelete('person')
await transaction.commit()
} catch (error) {
await transaction.rollback()
throw error
}
}
J'étais tombé sur ce multi-récidiviste :
async function deleteUser(req, res) {
return userService.destroyUser(req.params.id).then(async (user) => {
res.status(200).send(user)
})
}
Purée, y'a rien qui va.
async function deleteUser(req, res) {
const user = await userService.destroyUser(req.params.id)
res.status(200).send(user)
}
C'est une variante « moins grave » du mélange des styles, mais c'est quand même so 2015. Je suis tombé sur ce clusterfuck récemment :
export function findAllUsers(query) {
…
return User.findAndCountAll(…)
.then(ensureAtLeastOne)
.catch((error) => {
throw new Error(error)
})
}
- Il y a un risque de double mode d’erreur (synchrone et asynchrone).
- Ce
catch
est aussi utile que le H de Hawaï. - Les chaînes de promesses restent plus dures à orchestrer (pas de structures de contrôle).
function getUsersLastPost(userId) {
let user
let post
return User.findByPk(userId)
.then((u) => {
user = u
return u.posts.sort('-createdAt').findOne()
})
.then((p) => {
post = p
return p.comments.sort('-createdAt').limit(10).find()
})
.then((comments) => {
return { user, post, comments }
})
}
(Je sais, je me répète.)
export async function findAllUsers(query) {
…
return ensureAtLeastOne(await User.findAndCountAll())
}
// Sans doute optimisable par eager-loading, mais c'est un autre sujet,
// et on ne fait pas de N+1 en plus ici, alors bon.
async function getUsersLastPost(userId) {
const user = await User.findByPk(userId)
const post = await user.posts.sort('-createdAt').findOne()
const comments = await post.comments.sort('-createdAt').limit(10).find()
return { user, post, comments }
}
Alias « Eeeeh j'ai découvert Promise.resolve()
et Promise.reject()
! »
async (error) => {
…
return Promise.reject(error)
}
Mais pourquoi ?! Ta fonction async
enrobe automatiquement son code en promesse. Sers-t'en !
async (error) => {
…
throw error
}
Fonctions non async
car elles n'utilisent pas await
, mais censées renvoyer des promesses :
http.interceptors.response.use(
(response) => {
store.commit('loading/setLoading', false)
return Promise.resolve(response.data)
},
(error) => {
store.commit('loading/setLoading', false)
return Promise.reject(error)
}
)
-
Le
Promise.resolve
est plus explicite que de déclarer la fonctionasync
avec unreturn response.data
. -
Le
Promise.reject
est plus performant que de déclarer la fonctionasync
avec unthrow error
.
async
/await
est nettement supérieur aux chaînes manuellesawait
suspend, il ne bloque pas.await
est possible en racine de module (Top-Level Await, ou TLA) et dans le corps immédiat d'une fonctionasync
.- Toute fonction peut être
async
. - Les fonctions
async
enrobent implicitement leurs corps comme promesse. - Tu ne devrais jamais faire un
return await
(ou équivalent) hors d'untry…catch
.
<style> .feedback { display: flex; flex-direction: column; gap: 1em; align-items: center; } .feedback img { max-height: 7em; display: block; } .feedback p { margin: 0; } </style>Cette présentation est sur bit.ly/async-js-no-cringe
.
Crédits : photo de couverture par JESHOOTS.COM sur Unsplash