Javascript Event Loop

Facciamo chiarezza

Elvis Ciuffetelli
5 min readMar 10, 2021
Photo by Iva Rajović on Unsplash

Nota: Questo articolo si riferisce a JavaScript eseguito nell’ambiente del browser. I concetti sono comunque validi anche per Node.js, anche se Node.js usa API diverse rispetto alle Web API del browser.

L’Event Loop è uno degli argomenti più importanti da comprendere in JavaScript per capire cosa accade dietro le quinte delle nostre applicazioni e per gestire la programmazione asincrona. Questo articolo è un modo per fare chiarezza su questo topic, rivolto a tutti quelli che stiano iniziando a studiare JavaScript oppure vogliano approfondirne la conoscenza.

Introduzione

Molte volte ho sentito dire che “JavaScript è asincrono”. Invece, JavaScript è un linguaggio single-threaded, quindi è sincrono. Può processare una sola istruzione alla volta. Se un’istruzione richiede tempo per essere terminata, il programma bloccherà la sua esecuzione e di conseguenza l’utente non potrà scrollare, cliccare o digitare finché l’operazione in corso non sarà completata. Possiamo facilmente verificare questo comportamento sincrono eseguendo un ciclofor nestato:

L’esecuzione del ciclo impiega alcuni secondi, così non vedremo il console.log “I have to wait” fino a quando il ciclofornon sarà terminato. Questo codice causa un blocking nell’esecuzione del programma.

Nella realtà, quando utilizziamo qualsiasi web app moderna, non abbiamo però problemi di blocking. Ogni processo che necessita di tempo per completarsi, come richiedere dati tramite chiamate network, può impiegare un tempo variabile che dipende dalla mole di dati, dalla velocità della connessione internet e altri fattori. Se una chiamata API venisse eseguita in maniera sincrona, sperimenteremmo questi blocking.

Per evitare ciò, l’ambiente del browser espone molte Web API, a cui JavaScript può accedere, che sono asincrone; possono cioè essere eseguite in parallelo invece che in sequenza. Questo è necessario perché permette all’utente di continuare ad usare il browser normalmente mentre le operazioni asincrone vengono processate.

Event Loop

JavaScript gestisce il codice asincrono con l’event loop. I due pilastri dell’event loop sono il call stack e la coda (queue).

Call Stack

Il call stack è di tipo LIFO (Last In, First Out). L’ultimo elemento aggiunto è anche il primo ad essere rimosso dallo stack.

L’event loop controlla continuamente il call stack per vedere se c’è qualche funzione che bisogna eseguire.

Se eseguiamo questo codice:

const first = () => console.log("first")const second = () => console.log("second")const third = () => console.log("third")// Execute the functions
first()
second()
third()
//
first
second
third

Il browser gestisce l’esecuzione in questo modo:

  • Aggiunge la funzionefirst() allo stack, esegue first() che stampafirstnella console , rimuove first() dallo stack.
  • Aggiunge la funzione second() allo stack, esegue second() che stampasecondnella console , rimuove second() dallo stack.
  • Aggiunge la funzione third() allo stack, esegue third() che stampathirdnella console, rimuove third() dallo stack.

JavaScript eseguirà la funzione corrente nello stack, poi la rimuoverà e si sposterà sulla successiva.

Quando però eseguiamo codice asincrono, le cose sono diverse.

Un’API asincrona che possiamo testare facilmente a titolo di esempio èsetTimeout, che setta un timer ed esegue una funzione dopo un certo periodo di tempo. setTimeout deve essere asincrona, altrimenti il browser si freezerà durante l’attesa bloccando tutto il programma (esperienza utente terribile).

Usiamo quindi setTimeout nella funzionesecond per simulare una richiesta asincrona.

const first = () => console.log("first")const second = () => setTimeout(() => console.log("second"), 0)const third = () => console.log("third")// Execute the functions
first()
second()
third()

In questo codiceconsole.log viene eseguito in una funzione anonima e passata asetTimeout con il timer impostato a0 millisecondi.

Potremmo aspettarci che con unsetTimeout settato a0 queste tre funzioni diano comunque come risultato i console.log stampati in sequenza (first, second, third). Ma in realtà il console.log del setTimeout (third) verrà stampato per ultimo:

first
third
second

Questo è quello che fa JavaScript dietro le quinte in questo caso:

  • Aggiunge first() allo stack, esegue first() che stampafirstnella console, rimuovefirst() dallo stack.
  • Aggiunge second() allo stack, esegue second().
  • Aggiunge setTimeout() allo stack, esegue lasetTimeout() Web API che setta un timer e aggiunge la funzione anonima alla queue, rimuove setTimeout() dallo stack.
  • Rimuove second() dallo stack.
  • Aggiunge third() allo stack, esegue third() che stampathirdnella console, rimuovethird() dallo stack.
  • L’event loop controlla la queue (anche detta message queue) per cercare la presenza di qualche messaggio pending e trova la funzione anonima proveniente dasetTimeout(), aggiunge la funzione allo stack che stampasecondnella console, poi la rimuove dallo stack.

Non ha importanza che il timer sia impostato a zero oppure a dieci minuti: il console.log chiamato dal codice asincrono viene eseguito soltanto dopo le funzioni sincrone top-level. Questo accade perché JavaScript, in questo caso eseguito nel browser, usa l’event loop per gestire i processi paralleli. Dato che JavaScript può eseguire soltanto un’istruzione alla volta, ha bisogno di dire all’event loop quando eseguire un’istruzione specifica. L’event loop gestisce questo grazie allo stack e alla queue.

Queue

La queue, detta anche message queue o task queue, è un’area di attesa per le funzioni. Ogni volta che il call stack è vuoto, l’event loop controlla la queue per verificare la presenza di messaggi in waiting, iniziando dal messaggio più vecchio. Quando ne trova uno, lo aggiunge allo stack, che eseguirà la funzione nel messaggio.

Quando setTimeout viene invocata, il browser o Node.js inizializza il timer. Quando il timer scade, in questo caso immediatamente dato che lo abbiamo settato a 0, la funzione callback viene messa nella message queue.

Nella message queue vengono anche inseriti gli eventi triggerati dall’utente, come i click oppure gli eventi della tastiera, oppure le response delle fetch, prima che il codice reagisca a loro. Anche gli eventi DOM comeonLoad vengono inseriti nella queue.

L’event loop da priorità al call stack, processa prima tutto ciò che si trova nello stack, e quando non c’è più nulla (stack vuoto) controlla il message queue ed esegue le funzioni che si trovano lì.

Per controllare e gestire l’ordine delle esecuzioni in JavaScript, si utilizzano le callback, le Promise, e async/await. Capire l’event loop è necessario per poter in seguito padroneggiare queste tecniche di gestione dell’asincronia.

Nota: ECMAScript2015 ha introdotto il concetto di job queue o microtask queue, che viene usato dalle Promises (introdotte anch’esse in ES6/ES2015). I microtasks come le promises sono gestite con priorità maggiore rispetto ai macrotasks come setTimeout.

--

--

Elvis Ciuffetelli

Software developer with a passion for books and reading.