Cos'è l'Event Loop e come migliora le prestazioni dell'app?
Il modello di esecuzione di JavaScript è sfumato e facile da fraintendere. Conoscere il ciclo degli eventi al suo interno può aiutare.
JavaScript è un linguaggio a thread singolo, creato per gestire le attività una alla volta. Tuttavia, il ciclo degli eventi consente a JavaScript di gestire eventi e callback in modo asincrono emulando sistemi di programmazione simultanei. Ciò garantisce le prestazioni delle tue applicazioni JavaScript.
Cos'è il ciclo di eventi JavaScript?
Il ciclo di eventi di JavaScript è un meccanismo che viene eseguito in background di ogni applicazione JavaScript. Consente a JavaScript di gestire le attività in sequenza senza bloccare il thread di esecuzione principale. In questo caso si parla di programmazione asincrona.
Il ciclo di eventi mantiene una coda di attività da eseguire e invia tali attività all'API Web corretta per l'esecuzione una alla volta. JavaScript tiene traccia di queste attività e le gestisce in base al livello di complessità dell'attività.
Comprendere la necessità del ciclo di eventi JavaScript e della programmazione asincrona. Devi capire quale problema risolve essenzialmente.
Prendi questo codice, ad esempio:
function longRunningFunction() {
// This function does something that takes a long time to execute.
for (var i = 0; i < 100000; i++) {
console.log("Hello")
}
}
function shortRunningFunction(a) {
return a * 2 ;
}
function main() {
var startTime = Date.now();
longRunningFunction();
var endTime = Date.now();
// Prints the amount of time it took to execute functions
console.log(shortRunningFunction(2));
console.log("Time taken: " + (endTime - startTime) + " milliseconds");
}
main();
Questo codice definisce innanzitutto una funzione chiamata longRunningFunction(). Questa funzione eseguirà una sorta di attività complessa che richiede tempo. In questo caso, esegue un ciclo for ripetendo oltre 100.000 volte. Ciò significa che console.log("Hello") viene eseguito 100.000 volte.
A seconda della velocità del computer, questa operazione può richiedere molto tempo e bloccare l'esecuzione immediata di shortRunningFunction() fino al completamento della funzione precedente.
Per contesto, ecco un confronto del tempo impiegato per eseguire entrambe le funzioni:
E poi il singolo shortRunningFunction():
La differenza tra un'operazione da 2.351 millisecondi e un'operazione da 0 millisecondi è evidente quando si mira a creare un'app performante.
In che modo il ciclo di eventi aiuta con le prestazioni dell'app
Il ciclo degli eventi ha diverse fasi e parti che contribuiscono a far funzionare il sistema.
Lo stack di chiamate
Lo stack di chiamate JavaScript è essenziale per il modo in cui JavaScript gestisce le chiamate di funzioni ed eventi dalla tua applicazione. Il codice JavaScript viene compilato dall'alto verso il basso. Tuttavia, Node.js, leggendo il codice, Node.js assegnerà chiamate di funzione dal basso verso l'alto. Mentre legge, inserisce le funzioni definite come frame nello stack di chiamate una per una.
Lo stack di chiamate è responsabile del mantenimento del contesto di esecuzione e del corretto ordine delle funzioni. Lo fa operando come uno stack LIFO (Last-In-First-Out).
Ciò significa che l'ultimo frame di funzione che il programma inserisce nello stack di chiamate sarà il primo a uscire dallo stack ed essere eseguito. Ciò garantirà che JavaScript mantenga il giusto ordine di esecuzione delle funzioni.
JavaScript estrarrà ogni frame dallo stack finché non sarà vuoto, il che significa che tutte le funzioni avranno terminato l'esecuzione.
API WebLibuv
Al centro dei programmi asincroni di JavaScript c'è libuv. La libreria libuv è scritta nel linguaggio di programmazione C, che può interagire con le API di basso livello del sistema operativo. La libreria fornirà diverse API che consentono l'esecuzione del codice JavaScript in parallelo con altro codice. API per la creazione di thread, un'API per la comunicazione tra thread e un'API per la gestione della sincronizzazione dei thread.
Ad esempio, quando utilizzi setTimeout in Node.js per sospendere l'esecuzione. Il timer viene impostato tramite libuv, che gestisce il loop degli eventi per eseguire la funzione di callback una volta trascorso il ritardo specificato.
Allo stesso modo, quando si eseguono operazioni di rete in modo asincrono, libuv gestisce tali operazioni in modo non bloccante, garantendo che altre attività possano continuare l'elaborazione senza attendere la fine dell'operazione di input/output (I/O).
La coda di richiamate ed eventi
La coda di callback e di eventi è il luogo in cui le funzioni di callback attendono l'esecuzione. Quando un'operazione asincrona viene completata da libuv, la sua funzione di callback corrispondente viene aggiunta a questa coda.
Ecco come va la sequenza:
- JavaScript sposta le attività asincrone su libuv affinché possa gestirle e continua a gestire immediatamente l'attività successiva.
- Al termine dell'attività asincrona, JavaScript aggiunge la sua funzione di callback alla coda di callback.
- JavaScript continua a eseguire altre attività nello stack di chiamate finché non ha terminato tutto nell'ordine corrente.
- Una volta che lo stack di chiamate è vuoto, JavaScript esamina la coda di callback.
- Se c'è una richiamata in coda, inserisce la prima nello stack di chiamate e la esegue.
In questo modo, le attività asincrone non bloccano il thread principale e la coda di richiamata garantisce che le richiamate corrispondenti vengano eseguite nell'ordine in cui sono state completate.
Il ciclo del ciclo degli eventi
Il ciclo degli eventi ha anche qualcosa chiamato coda di microtask. Questa coda speciale nel ciclo degli eventi contiene microtask pianificati per l'esecuzione non appena viene completata l'attività corrente nello stack di chiamate. Questa esecuzione avviene prima del rendering successivo o dell'iterazione del ciclo di eventi. Le microattività sono attività ad alta priorità con precedenza rispetto alle attività regolari nel ciclo degli eventi.
Una microattività viene comunemente creata quando si lavora con Promises. Ogni volta che una Promessa viene risolta o rifiutata, i callback .then() o .catch() corrispondenti si uniscono alla coda delle microtask. Potresti utilizzare quella coda per gestire attività che richiedono un'esecuzione immediata dopo l'operazione corrente, come l'aggiornamento dell'interfaccia utente dell'applicazione o la gestione delle modifiche di stato.
Ad esempio, un'applicazione Web che esegue il recupero dei dati e aggiorna l'interfaccia utente in base ai dati recuperati. Gli utenti possono attivare il recupero dei dati facendo clic ripetutamente su un pulsante. Ogni clic sul pulsante avvia un'operazione asincrona di recupero dei dati.
Senza microtask, il ciclo di eventi per questa attività funzionerebbe come segue:
- L'utente fa clic ripetutamente sul pulsante.
- Ogni clic sul pulsante attiva un'operazione asincrona di recupero dei dati.
- Una volta completate le operazioni di recupero dei dati, JavaScript aggiunge i callback corrispondenti alla normale coda delle attività.
- Il ciclo di eventi avvia l'elaborazione delle attività nella normale coda delle attività.
- L'aggiornamento dell'interfaccia utente basato sui risultati del recupero dei dati viene eseguito non appena le attività regolari lo consentono.
Tuttavia, con i microtask, il ciclo degli eventi funziona in modo diverso:
- L'utente fa clic ripetutamente sul pulsante e attiva un'operazione di recupero dei dati asincrona.
- Una volta completate le operazioni di recupero dei dati, il ciclo di eventi aggiunge i callback corrispondenti alla coda delle microtask.
- Il ciclo di eventi avvia l'elaborazione delle attività nella coda delle microattività immediatamente dopo aver completato l'attività corrente (clic sul pulsante).
- L'aggiornamento dell'interfaccia utente basato sui risultati del recupero dei dati viene eseguito prima dell'attività regolare successiva, offrendo un'esperienza utente più reattiva.
Ecco un esempio di codice:
const fetchData = () => {
return new Promise(resolve => {
setTimeout(() => resolve('Data from fetch'), 2000);
});
};
document.getElementById('fetch-button').addEventListener('click', () => {
fetchData().then(data => {
// This UI update will run before the next rendering cycle
updateUI(data);
});
});
In questo esempio, ogni clic sul pulsante "Recupera" richiama fetchData(). Ogni operazione di recupero dei dati viene pianificata come microattività. In base ai dati recuperati, l'aggiornamento dell'interfaccia utente viene eseguito immediatamente dopo il completamento di ogni operazione di recupero, prima di qualsiasi altra attività di rendering o di ciclo di eventi.
Ciò garantisce che gli utenti visualizzino i dati aggiornati senza subire ritardi dovuti ad altre attività nel ciclo di eventi.
L'utilizzo di microtask in scenari come questo può prevenire errori dell'interfaccia utente e fornire interazioni più rapide e fluide nella tua applicazione.
Implicazioni dell'Event Loop per lo sviluppo web
Comprendere il ciclo degli eventi e come utilizzare le sue funzionalità è essenziale per creare applicazioni performanti e reattive. Il loop di eventi fornisce funzionalità asincrone e parallele, così puoi gestire in modo efficiente attività complesse nella tua applicazione senza compromettere l'esperienza dell'utente.
Node.js fornisce tutto ciò di cui hai bisogno, inclusi i web work per ottenere ulteriore parallelismo al di fuori del thread principale di JavaScript.