Apprendre JavaScript de zéro jusqu'au niveau expert : syntaxe moderne, DOM, asynchrone et event loop, modules, prototypes, métaprogrammation, performance et mémoire.
JavaScript est mono-thread : une seule ligne s'exécute à la fois. Pourtant, une page charge des données, réagit aux clics et anime des éléments en même temps. Le secret s'appelle l'event loop. C'est le chapitre le plus important du cours : sans lui, l'ordre d'exécution du code JavaScript reste un mystère.
console.log('A')
setTimeout(() => console.log('B'), 0)
console.log('C')
// A, C, BMême avec un délai de 0, B s'affiche en dernier. setTimeout ne met pas le code en pause : il confie le callback à l'environnement (navigateur ou Node) et continue.
┌───────────────────────────┐
│ Call stack │ ← code synchrone en cours
└─────────────┬─────────────┘
│ vide ?
▼
┌───────────────────────────┐
│ File des microtâches │ ← promesses (.then), queueMicrotask
│ (vidée ENTIÈREMENT) │
└─────────────┬─────────────┘
▼
┌───────────────────────────┐
│ Rendu (navigateur) │ ← requestAnimationFrame, style, paint
└─────────────┬─────────────┘
▼
┌───────────────────────────┐
│ File des tâches │ ← setTimeout, événements, I/O
│ (UNE tâche par tour) │
└───────────────────────────┘À chaque tour : la pile d'appels se vide, toutes les microtâches s'exécutent, le navigateur peut dessiner, puis une tâche est prise dans la file.
console.log('1 sync')
setTimeout(() => console.log('5 tâche'), 0)
Promise.resolve().then(() => console.log('3 microtâche'))
queueMicrotask(() => console.log('4 microtâche'))
console.log('2 sync')
// 1 sync, 2 sync, 3 microtâche, 4 microtâche, 5 tâcheConséquence pratique : une boucle de calcul de 3 secondes bloque tout, clics et animations compris. Le chapitre 11 montre comment découper ou déporter ce travail.
Le modèle historique : on passe une fonction appelée à la fin de l'opération.
import fs from 'node:fs'
fs.readFile('config.json', 'utf8', (err, contenu) => {
if (err) return console.error(err)
const config = JSON.parse(contenu)
fs.readFile(config.cheminDonnees, 'utf8', (err, donnees) => {
if (err) return console.error(err)
// et ainsi de suite…
})
})Imbriquer trois ou quatre opérations produit le fameux « callback hell », et la gestion d'erreur doit être répétée à chaque niveau.
Une promesse représente une valeur future. Elle est dans l'un de trois états : pending, fulfilled ou rejected, et ne change d'état qu'une fois.
const attendre = ms => new Promise(resolve => setTimeout(resolve, ms))
function chargerUser(id) {
return new Promise((resolve, reject) => {
if (id <= 0) reject(new Error('id invalide'))
else setTimeout(() => resolve({ id, nom: 'Ada' }), 200)
})
}
chargerUser(1)
.then(user => chargerCommandes(user.id))
.then(commandes => console.log(commandes))
.catch(err => console.error('Échec :', err.message))
.finally(() => masquerLoader())Chaque .then renvoie une nouvelle promesse : on enchaîne à plat, et un seul .catch attrape une erreur survenue n'importe où dans la chaîne.
Du sucre syntaxique sur les promesses, qui permet d'écrire du code asynchrone comme du code synchrone.
async function afficherCommandes(id) {
try {
const user = await chargerUser(id)
const commandes = await chargerCommandes(user.id)
console.log(commandes)
} catch (err) {
console.error('Échec :', err.message)
} finally {
masquerLoader()
}
}Une fonction async renvoie toujours une promesse. await suspend la fonction (pas le programme) jusqu'à la résolution.
async function getJSON(url, { timeout = 8000, ...options } = {}) {
const res = await fetch(url, { ...options, signal: AbortSignal.timeout(timeout) })
if (!res.ok) throw new Error(`HTTP ${res.status} sur ${url}`)
return res.json()
}
const articles = await getJSON('https://api.exemple.fr/articles')fetch ne rejette pas sur une réponse 404 ou 500 : seulement sur une erreur réseau. Vérifier res.ok est obligatoire.
// Séquentiel : ~600 ms
const a = await charger('a') // 200 ms
const b = await charger('b') // 200 ms
const c = await charger('c') // 200 ms
// Parallèle : ~200 ms
const [a2, b2, c2] = await Promise.all([charger('a'), charger('b'), charger('c')])Mettre await dans une boucle sur des opérations indépendantes est l'erreur de performance asynchrone la plus courante.
| Combinateur | Résout quand | Rejette quand |
|---|---|---|
Promise.all | toutes réussissent | la première échoue |
Promise.allSettled | toutes terminées | jamais |
Promise.race | la première termine | la première termine en échec |
Promise.any | la première réussit | toutes échouent |
const resultats = await Promise.allSettled(urls.map(u => getJSON(u)))
const ok = resultats.filter(r => r.status === 'fulfilled').map(r => r.value)
const ko = resultats.filter(r => r.status === 'rejected').map(r => r.reason.message)Promise.all sur 5 000 URLs lance 5 000 requêtes d'un coup. Pour en limiter le nombre :
async function mapLimite(items, limite, fn) {
const resultats = new Array(items.length)
let index = 0
async function worker() {
while (index < items.length) {
const i = index++
resultats[i] = await fn(items[i], i)
}
}
await Promise.all(Array.from({ length: limite }, worker))
return resultats
}
const pages = await mapLimite(urls, 10, url => getJSON(url))Comme JavaScript est mono-thread, index++ n'a aucun risque de concurrence : pas besoin de verrou.
let controller
async function rechercher(q) {
controller?.abort() // annule la requête précédente
controller = new AbortController()
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: controller.signal })
afficher(await res.json())
} catch (err) {
if (err.name !== 'AbortError') throw err
}
}Sans cela, une réponse lente à « jav » peut arriver après la réponse à « javascript » et écraser les bons résultats.
ES2024 simplifie la création de promesses contrôlées de l'extérieur :
const { promise, resolve } = Promise.withResolvers()
bouton.addEventListener('click', resolve, { once: true })
await promise
console.log('Bouton cliqué')Dans un module ES, await est autorisé en dehors d'une fonction :
export const config = await fetch('/config.json').then(r => r.json())Les modules qui importent config.js attendent sa résolution.
// Node
process.on('unhandledRejection', err => {
console.error('Promesse rejetée non gérée', err)
process.exit(1)
})
// Navigateur
window.addEventListener('unhandledrejection', e => reportError(e.reason))Une promesse sans .catch ni try/await qui échoue fait planter les versions récentes de Node. Tant mieux : une erreur silencieuse est pire qu'un crash.
const attendre = ms => new Promise(r => setTimeout(r, ms))
async function retry(fn, { tentatives = 4, base = 300 } = {}) {
for (let i = 0; ; i++) {
try {
return await fn()
} catch (err) {
if (i >= tentatives - 1) throw err
const delai = base * 2 ** i + Math.random() * 100
console.warn(`Tentative ${i + 1} échouée, nouvel essai dans ${Math.round(delai)} ms`)
await attendre(delai)
}
}
}
const data = await retry(() => getJSON('https://api.instable.fr/data'))Le return await dans le try est indispensable : sans await, la promesse serait renvoyée avant d'avoir échoué, et le catch ne verrait jamais l'erreur.
Une seule pile, une file de microtâches vidée entièrement, une file de tâches prise une par une. Tout l'asynchrone JavaScript découle de ce schéma. Quand un comportement vous surprend, redessinez-le : la réponse y est toujours.