Funktionsweise von Node.js

Abgeschlossen

In dieser Lerneinheit wird erläutert, wie Node.js in die JavaScript-Runtime eingehende Aufgaben verarbeitet.

Arten von Aufgaben

JavaScript-Anwendungen haben zwei Arten von Aufgaben:

  • Synchrone Aufgaben: Diese Aufgaben werden in einer bestimmten Reihenfolge ausgeführt. Sie sind nicht davon abhängig, dass eine andere Ressource abgeschlossen wird. Beispiele sind mathematische Operationen oder Zeichenfolgenmanipulation.
  • Asynchrone Aufgaben: Diese Aufgaben werden möglicherweise nicht sofort abgeschlossen, da sie von anderen Ressourcen abhängig sind. Beispiele sind Netzwerkanforderungen oder Dateisystemvorgänge.

Da Sie möchten, dass Ihr Programm so schnell wie möglich ausgeführt wird, möchten Sie, dass die JavaScript-Engine weiterarbeiten kann, während sie auf eine Antwort von einem asynchronen Vorgang wartet. Dazu fügt es die asynchronen Aufgaben in eine Aufgabenwarteschlange ein und fährt mit der Arbeit an der nächsten Aufgabe fort.

Verwalten der Aufgabenwarteschlange mit einer Ereignisschleife

Node.js verwendet die ereignisgesteuerte Architektur der JavaScript-Engine, um asynchrone Anforderungen zu verarbeiten. Die folgende Abbildung gibt einen allgemeinen Überblick über die Funktionsweise der V8-Ereignisschleife:

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

Eine asynchrone Aufgabe mit der entsprechenden Syntax (siehe unten) wird zur Ereignisschleife hinzugefügt. Die Aufgabe enthält die zu erledigenden Aufgaben und eine Rückruffunktion zum Empfangen der Ergebnisse. Nach Abschluss dieses aufwendigen Vorgangs wird die Rückruffunktion mit den Ergebnissen ausgelöst.

Vergleich zwischen synchronen und asynchronen Vorgängen

Die Node.js-APIs bieten für manche Vorgänge sowohl asynchrone als auch synchrone Vorgänge an, z. B. für Dateivorgänge. In der Regel sollten Sie zwar immer asynchrone Vorgänge bevorzugen, es gibt aber Situationen, in denen die Verwendung synchroner Vorgänge sinnvoll sein kann.

Ein Beispiel wäre eine Situation, in der eine Befehlszeilenschnittstelle (Command Line Interface, CLI) eine Datei liest und die Daten in der Datei dann sofort verwendet. In diesem Fall können Sie die synchrone Version des Dateivorgangs verwenden, da weder ein anderes System noch eine andere Person darauf warten, die Anwendung verwenden zu können.

Wenn Sie jedoch einen Webserver erstellen, sollten Sie immer die asynchrone Version des Dateivorgangs verwenden, um die Fähigkeit zur Ausführung des einzelnen Threads zum Verarbeiten anderer Benutzeranforderungen nicht zu blockieren.

In Ihrer Arbeit als Entwickler bei TailWind Traders müssen Sie den Unterschied zwischen synchronen und asynchronen Vorgängen und ihre Verwendungszwecke verstehen.

Leistung durch asynchrone Vorgänge

Node.js macht sich die einzigartige ereignisgesteuerte Natur von JavaScript zunutze, die das Zusammenstellen von Serveraufgaben schnell und leistungsstark macht. JavaScript kann dank der Leistungsverbesserungen durch die V8-Engine bei korrekter Verwendung mit asynchronen Techniken dieselben Leistungsergebnisse wie Low-Level-Programmiersprachen (z. B. C) erzielen.

Es gibt drei Stile asynchroner Techniken, die Sie bei Ihrer Arbeit erkennen können müssen:

  • Asynchron/warten (empfohlen): Die neueste asynchrone Technik, die die async- und await-Schlüsselwörter verwendet, um die Ergebnisse eines asynchronen Vorgangs zu empfangen. Async/await wird in vielen Programmiersprachen verwendet. Neue Projekte mit neueren Abhängigkeiten verwenden im Allgemeinen diesen asynchronen Codestil.
  • Rückrufe: Die ursprüngliche asynchrone Technik, die eine Rückruffunktion verwendet, um die Ergebnisse eines asynchronen Vorgangs zu empfangen. Diese Technik begegnet Ihnen in älteren Codebasen und Node.js-APIs.
  • Zusagen: Eine neuere asynchrone Technik, die ein Promise-Objekt verwendet, um die Ergebnisse eines asynchronen Vorgangs zu empfangen. Diese Technik begegnet Ihnen in neueren Codebasen und Node.js-APIs. Möglicherweise müssen Sie bei Ihrer Arbeit auf Zusagen basierenden Code schreiben, um ältere APIs zu umschließen, die nicht aktualisiert werden. Wenn Sie Zusagen für diese Umschließungen verwenden, können Sie den Code in node.js-versionierten Projekten in größerem Umfang verwenden als im neueren asynchronen/await-Codestil.

Async/await

Asynchron/warten ist die neueste Art, asynchrone Programmierung zu handhaben. Async/await ist sozusagen die syntaktische Sahne auf der Zusagen-Torte und lässt asynchronen Code eher wie synchronen Code aussehen. Sie ist auch einfacher zu lesen und zu verwalten.

Das gleiche Beispiel mit async/await sieht wie folgt aus:

// 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);
  });

Als asynchron/warten in ES2017 freigegeben wurde, konnten die Schlüsselwörter nur in Funktionen verwendet werden, bei der die Funktion der obersten Ebene eine Zusage war. Obwohl die Zusage nicht über then- und catch-Abschnitte verfügen musste, war es dennoch erforderlich, eine promise-Syntax für die Ausführung zu haben.

Eine async-Funktion gibt immer eine Zusage zurück, auch wenn sie keinen await-Aufruf darin hat. Die Zusage wird mit dem von der Funktion zurückgegebenen Wert aufgelöst. Wenn die Funktion einen Fehler auslöst, wird die Zusage mit dem ausgelösten Wert abgelehnt.

Promises

Da verschachtelte Rückrufe schwierig zu lesen und zu verwalten sein können, hat Node.js Unterstützung für Zusagen hinzugefügt. Eine Zusage ist ein Objekt, das den finalen Abschluss (oder Fehlschlag) eines asynchronen Vorgangs darstellt.

Eine Promise-Funktion hat das folgende Format:

// 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
  });

Die then-Methode wird aufgerufen, wenn die Zusage erfüllt ist und die catch-Methode aufgerufen wird, wenn die Zusage abgelehnt wird.

Um eine Datei asynchron mit Zusagen zu lesen, lautet der Code:

// 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 der obersten Ebene

In den neuesten Versionen von Node.js wurde async/await auf oberster Ebene für ES6-Module hinzugefügt. Sie müssen eine Eigenschaft namens type mit dem Wert module zu package.json hinzufügen, um dieses Feature zu verwenden.

{
    "type": "module"
}

Anschließend können Sie das await-Schlüsselwort auf der obersten Ebene Ihres Codes verwenden.

// 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!");

Rückrufe

Bei der ursprünglichen Veröffentlichung von Node.js wurde die asynchrone Programmierung mit Hilfe von Rückruffunktionen durchgeführt. Rückrufe sind Funktionen, die als Argumente an andere Funktionen übergeben werden. Wenn die Aufgabe abgeschlossen ist, wird die Rückruffunktion aufgerufen.

Die Reihenfolge der Parameter der Funktion ist wichtig. Die Rückruffunktion ist der letzte Parameter der Funktion.

// Callback function is the last parameter
function(param1, param2, paramN, callback)

Der Funktionsname im von Ihnen verwalteten Code ist möglicherweise nicht callback. Es könnte auch cb, done oder next lauten. Der Name der Funktion ist nicht wichtig, die Reihenfolge der Parameter aber schon.

Beachten Sie, dass es keinen syntaktischen Hinweis dafür gibt, dass die Funktion asynchron ist. Sie müssen wissen, dass die Funktion asynchron ist, indem Sie die Dokumentation lesen oder den Code durchgehen.

Beispiel für einen Rückruf mit benannter Rückruffunktion

Der folgende Code trennt die asynchrone Funktion vom Rückruf. Das macht ihn leicht lesbar und verständlich. Außerdem haben Sie so die Möglichkeit, den Rückruf für andere asynchrone Funktionen wiederzuverwenden.

// 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!");

Das korrekte Ergebnis lautet:

I'm the last line of the file!
Hi, developers!
Done!

Zunächst wird die asynchrone Funktion fs.readFile gestartet und erreicht die Ereignisschleife. Anschließend wird die Codeausführung mit der nächsten Codezeile fortgesetzt, was die letzte console.log ist. Nachdem die Datei gelesen wurde, wird die Rückruffunktion aufgerufen, und die beiden console.log-Anweisungen werden ausgeführt.

Beispiel für einen Rückruf mit anonymer Funktion

Im folgenden Beispiel wird eine anonyme Rückruffunktion verwendet. Das bedeutet, dass die Funktion keinen Namen hat und von anderen anonymen Funktionen nicht wiederverwendet werden kann.

// 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!");

Das korrekte Ergebnis lautet:

I'm the last line of the file!
Hi, developers!
Done!

Wenn der Code ausgeführt wird, wird zunächst die asynchrone Funktion fs.readFile gestartet und erreicht die Ereignisschleife. Als Nächstes wird die Ausführung mit der folgenden Codezeile fortgesetzt, was die letzte console.log ist. Wenn die Datei gelesen wird, wird die Rückruffunktion aufgerufen, und die beiden console.log-Anweisungen werden ausgeführt.

Geschachtelte Rückrufe

Da Sie möglicherweise einen nachfolgenden asynchronen Rückruf und dann einen anderen aufrufen müssen, entsteht möglicherweise ein geschachtelter Rückrufcode. Solcher Code wird scherzhaft als Rückruf-Hölle (callback hell) bezeichnet, da er schwer zu lesen und zu verwalten ist.

// 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
          }
        });
      }
    });
  }
});

Synchrone APIs

Node.js verfügt auch über eine Reihe synchroner APIs. Diese APIs blockieren die Ausführung des Programms, bis die Aufgabe abgeschlossen ist. Synchrone APIs sind nützlich, wenn Sie eine Datei lesen und dann sofort die Daten aus der Datei verwenden möchten.

Synchrone (blockierende) Funktionen in Node.js verwenden die Namenskonvention functionSync. Die asynchrone readFile-API verfügt beispielsweise über ein synchrones Gegenstück mit dem Namen readFileSync. Es ist wichtig, diesen Standard in Ihren eigenen Projekten aufrechtzuerhalten, damit Ihr Code leicht zu lesen und zu verstehen ist.

// 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);
}

Als neuer Entwickler bei TailWind Traders werden Sie möglicherweise aufgefordert, einen beliebigen Node.js-Codetyp zu ändern. Es ist wichtig, den Unterschied zwischen synchronen und asynchronen APIs sowie den verschiedenen Syntaxen für asynchronen Code zu verstehen.