Skip to content

Latest commit

 

History

History
512 lines (376 loc) · 11.2 KB

slides.md

File metadata and controls

512 lines (376 loc) · 11.2 KB
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 !
persist syncAll
unocss

L’asynchrone en JS
sans le cringe

Une présentation de Christophe Porteneuve


whoami

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'
  ]
}

layout: center

Usual Suspects


async sans await ni enrobage promesse voulu

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)
}

async sans await ni enrobage promesse voulu : fix

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)
}

map à tort sur un callback async

const mailSequence = mails.map(async (mail) => await sendMail(mail))

map à tort sur un callback async : fix n°1

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)

map à tort sur un callback async : fix n°2

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))
}

return await superflu (ou son équivalent)

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
}

return await superflu (ou son équivalent) : fix

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 })
}

Contre-exemple pour await suivi de return

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
}

Le cas légitime pour return await

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.


Séquencer au lieu de paralléliser

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 })
  }
  
}

Séquencer au lieu de paralléliser : fix

Cadeau bonus : ça permet dans ce cas précis de virer cette fichue boucle numérique qui aurait dû être une jolie forof. 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 }))

Mélanger .then() et async / await

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
  }
}

Mélanger .then() et async / await : fix

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
  }
}

ZOMGWTFBBQ

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)
}

Utiliser des chaînes de promesses manuelles

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).

Aparté : scope juggling dans une chaîne manuelle

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 }
    })
}

Utilise async / await

(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 }
}

Enrobage manuel à tort ou superflu

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
}

Contre-exemple : enrobage manuel intentionnel

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 fonction async avec un return response.data.

  • Le Promise.reject est plus performant que de déclarer la fonction async avec un throw error.


En résumé…

  • async / await est nettement supérieur aux chaînes manuelles
  • await 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 fonction async.
  • 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'un try…catch

layout: cover background: /jeshoots-com--2vD8lIhdnw-unsplash.jpg

Merci ! 🤗

.

<style> .feedback { display: flex; flex-direction: column; gap: 1em; align-items: center; } .feedback img { max-height: 7em; display: block; } .feedback p { margin: 0; } </style>

Crédits : photo de couverture par JESHOOTS.COM sur Unsplash