Come funziona Node.js
Questa unità illustra il modo in cui Node.js gestisce le attività in ingresso per il runtime JavaScript.
Tipi di attività
Le applicazioni JavaScript hanno due tipi di attività:
- Attività sincrone: queste attività vengono eseguite in ordine. Il loro completamento non dipende da un'altra risorsa. Ne sono esempi le operazioni matematiche o la manipolazione di stringhe.
- Attività asincrone: Queste attività potrebbero non essere completate immediatamente perché dipendono da altre risorse. Ne sono esempi le richieste di rete o le operazioni del file system.
Affinché il programma venga eseguito il più velocemente possibile, è necessario che il motore JavaScript possa continuare a funzionare mentre attende una risposta da un'operazione asincrona. A tale scopo, aggiunge l'attività asincrona a una coda di attività e continua a lavorare sull'attività successiva.
Gestire la coda di attività con il ciclo di eventi
Node.js usa l'architettura guidata dagli eventi del motore JavaScript per elaborare le richieste asincrone. Il diagramma seguente illustra il funzionamento generale del ciclo di eventi V8:
Un'attività asincrona, indicata dalla sintassi appropriata (illustrata di seguito), viene aggiunta al ciclo di eventi. L'attività include il lavoro da eseguire e una funzione di callback per ricevere i risultati. Al termine dell'operazione intensiva, la funzione di callback viene attivata con i risultati.
Operazioni sincrone e asincrone a confronto
Le API di Node.js forniscono sia operazioni asincrone che sincrone per alcune delle stesse operazioni, ad esempio quelle sui file. Anche se in genere è consigliabile dare sempre la priorità alle operazioni asincrone, in alcuni casi è possibile usare operazioni sincrone.
Un esempio è quando un'interfaccia della riga di comando legge un file e quindi usa immediatamente i dati nel file. In questo caso, è possibile usare la versione sincrona dell'operazione sul file perché non esiste un altro sistema o persona in attesa di usare l'applicazione.
Tuttavia, se si sta creando un server Web, è consigliabile usare sempre la versione asincrona dell'operazione su file per non bloccare la capacità di esecuzione del thread singolo per elaborare altre richieste utente.
Nel proprio lavoro come sviluppatore di TailWind Traders, è necessario comprendere la differenza tra le operazioni sincrone e asincrone e quando usarle.
Prestazioni tramite operazioni asincrone
Node.js sfrutta la natura unica guidata dagli eventi di JavaScript, velocizzando e migliorando le prestazioni della composizione delle attività del server. JavaScript, se usato correttamente con le tecniche asincrone, può offrire gli stessi risultati a livello di prestazioni di linguaggi di basso livello, come C, grazie alle ottimizzazioni delle prestazioni rese possibili dal motore V8.
Le tecniche asincrone sono disponibili in 3 stili, che è necessario essere in grado di riconoscere nel lavoro:
- Async/await (stile consigliato): La tecnica asincrona più recente che usa le parole chiave
async
eawait
per ricevere i risultati di un'operazione asincrona. Async/await viene usato in molti linguaggi di programmazione. In genere, i nuovi progetti con dipendenze più recenti useranno questo stile di codice asincrono. - Callback: La tecnica asincrona originale che usa una funzione di callback per ricevere i risultati di un'operazione asincrona. Si potrà vedere questa tecnica nelle codebase meno recenti e nelle API di Node.js precedenti.
- Promesse: La tecnica asincrona più recente che usa un oggetto promessa per ricevere i risultati di un'operazione asincrona. Si potrà vedere questa tecnica nelle codebase e nelle API di Node.js più recenti. Potrebbe essere necessario scrivere codice basato su promesse nel lavoro per eseguire il wrapping delle API meno recenti che non verranno aggiornate. Usando promesse per questo wrapping, è possibile usare il codice in un gamma più ampia di progetti con controllo delle versioni di Node.js rispetto allo stile del codice async/await più recente.
Async/await
Async/await è un modo più recente per gestire la programmazione asincrona. Async/await si basa sulle promesse e rende il codice asincrono più simile al codice sincrono. È anche più facile leggere e gestire.
Lo stesso esempio, con async/await, è simile al seguente:
// async/await asynchronous example
const fs = require('fs').promises;
const filePath = './file.txt';
// `async` before the parent function
async function readFileAsync() {
try {
// `await` before the async method
const data = await fs.readFile(filePath, 'utf-8');
console.log(data);
console.log('Done!');
} catch (error) {
console.log('An error occurred...: ', error);
}
}
readFileAsync()
.then(() => {
console.log('Success!');
})
.catch((error) => {
console.log('An error occurred...: ', error);
});
Quando async/await è stato rilasciato in ES2017, le parole chiave potevano essere usate solo nelle funzioni con la funzione di primo livello che rappresenta una promessa. Anche se la promessa non doveva avere le sezioni then
e catch
, per l'esecuzione doveva comunque avere la sintassi promise
.
Una funzione async
restituisce sempre una promessa, anche se non ha una chiamata await
al suo interno. La promessa verrà risolta con il valore restituito dalla funzione. Se la funzione genera un errore, la promessa verrà rifiutata con il valore generato.
Suggerimenti
Poiché può essere difficile leggere e gestire i callback annidati, Node.js ha aggiunto il supporto per le promesse. Una promessa è un oggetto che rappresenta il completamento finale (o l'errore) di un'operazione asincrona.
Una funzione promessa ha questo formato:
// Create a basic promise function
function promiseFunction() {
return new Promise((resolve, reject) => {
// do something
if (error) {
// indicate success
reject(error);
} else {
// indicate error
resolve(data);
}
});
}
// Call a basic promise function
promiseFunction()
.then((data) => {
// handle success
})
.catch((error) => {
// handle error
});
Il metodo then
viene chiamato quando la promessa viene soddisfatta, invece il metodo catch
viene chiamato quando la promessa viene rifiutata.
Per leggere un file in modo asincrono con promesse, il codice è:
// promises asynchronous example
const fs = require('fs').promises;
const filePath = './file.txt';
// request to read a file
fs.readFile(filePath, 'utf-8')
.then((data) => {
console.log(data);
console.log('Done!');
})
.catch((error) => {
console.log('An error occurred...: ', error);
});
console.log(`I'm the last line of the file!`);
Async/await di primo livello
Le versioni più recenti di Node.js hanno aggiunto async/await di primo livello per i moduli ES6. È necessario aggiungere una proprietà denominata type
in package.json con un valore di module
per usare questa funzionalità.
{
"type": "module"
}
È quindi possibile usare la parola chiave await
al livello superiore del codice.
// top-level async/await asynchronous example
const fs = require('fs').promises;
const filePath = './file.txt';
// `async` before the parent function
try {
// `await` before the async method
const data = await fs.readFile(filePath, 'utf-8');
console.log(data);
console.log('Done!');
} catch (error) {
console.log('An error occurred...: ', error);
}
console.log("I'm the last line of the file!");
Callback
In origine, quando Node.js è stato rilasciato, la programmazione asincrona veniva gestita usando le funzioni di callback. I callback sono funzioni che vengono passate come argomenti ad altre funzioni. Quando l'attività è completa, viene chiamata la funzione di callback.
L'ordine dei parametri della funzione è importante. La funzione di callback è l'ultimo parametro della funzione.
// Callback function is the last parameter
function(param1, param2, paramN, callback)
Il nome della funzione nel codice gestito potrebbe non essere chiamato callback
. Può essere chiamato cb
o done
o next
. Il nome della funzione non è importante, ma l'ordine dei parametri è importante.
Si noti che non esiste alcuna indicazione sintattica che la funzione sia asincrona. È necessario scoprire che la funzione è asincrona leggendo la documentazione o continuando a leggere il codice.
Esempio di callback con funzione callback denominata
Il codice seguente separa la funzione asincrona dal callback. Questo è facile da leggere e comprendere e consente di riutilizzare il callback per altre funzioni asincrone.
// callback asynchronous example
// file system module from Node.js
const fs = require('fs');
// relative path to file
const filePath = './file.txt';
// callback
const callback = (error, data) => {
if (error) {
console.log('An error occurred...: ', error);
} else {
console.log(data); // Hi, developers!
console.log('Done!');
}
};
// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function
fs.readFile(filePath, 'utf-8', callback);
console.log("I'm the last line of the file!");
Il risultato corretto è:
I'm the last line of the file!
Hi, developers!
Done!
In primo luogo, la funzione asincrona fs.readFile
viene avviata e passa al ciclo di eventi. L'esecuzione del codice continua quindi con la riga di codice successiva, ovvero l'ultima console.log
. Dopo la lettura del file, viene chiamata la funzione di callback e vengono eseguite le due istruzioni console.log.
Esempio di callback con funzione anonima
L'esempio seguente usa una funzione di callback anonima, il che significa che la funzione non ha un nome e non può essere riutilizzata da altre funzioni anonime.
// callback asynchronous example
// file system module from Node.js
const fs = require('fs');
// relative path to file
const filePath = './file.txt';
// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function () => {}
fs.readFile(filePath, 'utf-8', (error, data) => {
if (error) {
console.log('An error occurred...: ', error);
} else {
console.log(data); // Hi, developers!
console.log('Done!');
}
});
console.log("I'm the last line of the file!");
Il risultato corretto è:
I'm the last line of the file!
Hi, developers!
Done!
Quando viene eseguito il codice, la funzione asincrona fs.readFile
viene avviata e passa al ciclo di eventi. L'esecuzione del codice continua quindi con la riga di codice successiva, ovvero l'ultima console.log
. Quando viene letto il file, viene chiamata la funzione di callback e vengono eseguite le due istruzioni console.log.
Callback annidati
Poiché potrebbe essere necessario chiamare un callback asincrono successivo e quindi un altro, il codice di callback potrebbe diventare annidato. Si tratta di una situazione nota come l'inferno dei callback ed è difficile da leggere e gestire.
// nested callback example
// file system module from Node.js
const fs = require('fs');
fs.readFile(param1, param2, (error, data) => {
if (!error) {
fs.writeFile(paramsWrite, (error, data) => {
if (!error) {
fs.readFile(paramsRead, (error, data) => {
if (!error) {
// do something
}
});
}
});
}
});
API sincrone
Node.js include anche un set di API sincrone. Queste API bloccano l'esecuzione del programma fino al completamento dell'attività. Le API sincrone sono utili quando si vuole leggere un file e quindi usare immediatamente i dati del file.
Le funzioni sincrone (di blocco) in Node.js usano la convenzione di denominazione functionSync
. Ad esempio, l'API asincrona readFile
ha una controparte sincrona denominata readFileSync
. È importante rispettare questo standard nei progetti in modo che il codice sia facilmente leggibile e comprensibile.
// synchronous example
const fs = require('fs');
const filePath = './file.txt';
try {
// request to read a file
const data = fs.readFileSync(filePath, 'utf-8');
console.log(data);
console.log('Done!');
} catch (error) {
console.log('An error occurred...: ', error);
}
In qualità di nuovo sviluppatore di TailWind Traders, potrebbe essere richiesto di modificare qualsiasi tipo di codice Node.js. È importante comprendere la differenza tra le API sincrone e asincrone e le diverse sintassi per il codice asincrono.