Come funziona Node.js

Completato

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.
  • Asincrona: 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 del ciclo di eventi V8, a livello generale:

Diagram showing how Node J S uses an event-driven architecture where an event loop processes events and returns callbacks.

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 a elevato utilizzo, la funzione di callback viene attivata con i risultati.

Operazioni sincrone e operazioni asincrone

Le API Node.js forniscono sia operazioni asincrone che sincrone per alcune delle stesse operazioni, ad esempio le operazioni sui file. Anche se in genere è consigliabile pensare sempre in modo asincrono, 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 di 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 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 tecniche asincrone, può produrre gli stessi risultati delle prestazioni dei linguaggi di basso livello come C a causa di miglioramenti delle prestazioni resi possibili dal motore V8.

Le tecniche asincrone sono disponibili in 3 stili, che è necessario essere in grado di riconoscere nel lavoro:

  • Async/await (scelta consigliata): la tecnica asincrona più recente che usa le async parole chiave e await 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: tecnica asincrona originale che usa una funzione di callback per ricevere i risultati di un'operazione asincrona. Questo problema verrà visualizzato nelle codebase precedenti e nelle API Node.js precedenti.
  • Promesse: una tecnica asincrona più recente che usa un oggetto promise per ricevere i risultati di un'operazione asincrona. Questo problema verrà visualizzato nelle codebase più recenti e nelle API 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 le promesse per questo wrapping, è possibile usare il codice in un intervallo più ampio di progetti con versione Node.js rispetto allo stile di codice asincrono/await più recente.

Async/await

Async/await è un modo più recente per gestire la programmazione asincrona. Async/await è zucchero sintattico sopra le 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 possono essere usate solo nelle funzioni con la funzione di primo livello che rappresenta una promessa. Anche se la promessa non doveva avere then e catch sezioni, era comunque necessario disporre promise della sintassi per l'esecuzione.

Una funzione restituisce async sempre una promessa, anche se non ha una await chiamata 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é i callback annidati possono essere difficili da leggere e gestire, 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 promise ha il formato di:

// 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 await parola chiave 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 è asincrona. È necessario sapere 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!

Prima di tutto, la funzione fs.readFile asincrona viene avviata e passa al ciclo di eventi. L'esecuzione del codice continua quindi alla 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 fs.readFile asincrona viene avviata e passa al ciclo di eventi. Successivamente, l'esecuzione continua con la riga di codice seguente, ovvero l'ultima console.log. Quando il file viene letto, 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. Questo è chiamato callback inferno 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 di 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.