A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://developer.mozilla.org/de/docs/Web/JavaScript/Guide/Resource_management below:

JavaScript-Ressourcenmanagement - JavaScript | MDN

JavaScript-Ressourcenmanagement

Dieser Leitfaden behandelt, wie man in JavaScript Ressourcenmanagement betreibt. Ressourcenmanagement ist nicht genau dasselbe wie das Speicherverwaltung, ein fortgeschritteneres Thema, das normalerweise automatisch von JavaScript gehandhabt wird. Ressourcenmanagement bezieht sich auf die Verwaltung von Ressourcen, die nicht automatisch von JavaScript aufgeräumt werden. Manchmal ist es in Ordnung, einige ungenutzte Objekte im Speicher zu haben, da sie nicht die Anwendungslogik stören, aber Ressourcenausfälle führen oft dazu, dass Dinge nicht funktionieren oder eine Menge überschüssiger Speicherplatz genutzt wird. Daher ist dies keine optionale Eigenschaft zur Optimierung, sondern eine Kernfunktion zum Schreiben korrekter Programme!

Hinweis: Während Speicherverwaltung und Ressourcenmanagement zwei separate Themen sind, können Sie manchmal auf das System der Speicherverwaltung zugreifen, um Ressourcen zu verwalten, als letzten Ausweg. Wenn Sie beispielsweise ein JavaScript-Objekt haben, das einen Handle einer externen Ressource darstellt, können Sie ein FinalizationRegistry erstellen, um die Ressource zu bereinigen, wenn der Handle vom Garbage Collector entfernt wird, da es definitiv keinen Weg gibt, danach auf die Ressource zuzugreifen. Es gibt jedoch keine Garantie dafür, dass der Finalizer ausgeführt wird, daher ist es keine gute Idee, sich für kritische Ressourcen darauf zu verlassen.

Problem

Lassen Sie uns zunächst einige Beispiele von Ressourcen betrachten, die verwaltet werden müssen:

Hier ist ein konkretes Beispiel, bei dem ein lesbarer Stream verwendet wird:

const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("a");
    controller.enqueue("b");
    controller.enqueue("c");
    controller.close();
  },
});

async function readUntil(stream, text) {
  const reader = stream.getReader();
  let chunk = await reader.read();

  while (!chunk.done && chunk.value !== text) {
    console.log(chunk);
    chunk = await reader.read();
  }
  // We forgot to release the lock here
}

readUntil(stream, "b").then(() => {
  const anotherReader = stream.getReader();
  // TypeError: ReadableStreamDefaultReader constructor can only
  // accept readable streams that are not yet locked to a reader
});

Hier haben wir einen Stream, der drei Datenblöcke übernimmt. Wir lesen vom Stream, bis wir den Buchstaben "b" finden. Wenn readUntil zurückkehrt, ist der Stream nur teilweise gelesen, sodass wir in der Lage sein sollten, mit einem anderen Leser weiter zu lesen. Wir haben jedoch vergessen, das Lock zu lösen, sodass der Stream immer noch gesperrt ist und kein weiterer Leser erstellt werden kann.

Die Lösung in diesem Fall ist einfach: Rufen Sie reader.releaseLock() am Ende von readUntil auf. Aber einige Probleme bleiben bestehen:

Sie sehen, wie eine scheinbar harmlose Aufgabe des Aufrufens von releaseLock schnell zu einem verschachtelten Boilerplate-Code führen kann. Deshalb bietet JavaScript integrierte Sprachunterstützung für das Ressourcenmanagement.

Die using- und await using-Deklarationen

Die Lösung, die wir haben, sind zwei spezielle Arten der Variablendeklaration: using und await using. Sie sind const ähnlich, aber sie geben die Ressource automatisch frei, wenn die Variable aus dem Gültigkeitsbereich austritt, solange die Ressource freigebbar ist. Am gleichen Beispiel wie oben können wir es umschreiben als:

{
  using reader1 = stream1.getReader();
  using reader2 = stream2.getReader();

  // do something with reader1 and reader2

  // Before we exit the block, reader1 and reader2 are automatically released
}

Hinweis: Zum Zeitpunkt des Schreibens implementiert ReadableStreamDefaultReader das disposable-Protokoll nicht. Dies ist ein hypothetisches Beispiel.

Beachten Sie zuerst die zusätzlichen geschwungenen Klammern um den Code. Dies erzeugt einen neuen Blockbereich für die using-Deklarationen. Mit using deklarierte Ressourcen werden automatisch freigegeben, wenn sie aus dem Gültigkeitsbereich von using austreten, was in diesem Fall der Fall ist, wenn wir den Block verlassen, entweder weil alle Anweisungen ausgeführt wurden oder weil irgendwo ein Fehler, ein return/break/continue aufgetreten ist.

Dies bedeutet, dass using nur in einem Gültigkeitsbereich verwendet werden kann, der eine klare Lebensdauer hat – nämlich, es kann nicht auf der obersten Ebene eines Skripts verwendet werden, da Variablen auf der obersten Ebene eines Skripts im Gültigkeitsbereich für alle zukünftigen Skripte auf der Seite sind, was praktisch bedeutet, dass die Ressource niemals freigegeben werden kann, wenn die Seite niemals entladen wird. Sie können es jedoch auf der obersten Ebene eines Moduls verwenden, weil der Modulbereich endet, wenn das Modul die Ausführung beendet.

Jetzt wissen wir, wann using das Aufräumen durchführt. Aber wie wird es gemacht? using erfordert, dass die Ressource das disposable-Protokoll implementiert. Ein Objekt ist disposable, wenn es die Methode [Symbol.dispose]() hat. Diese Methode wird ohne Argumente aufgerufen, um die Bereinigung durchzuführen. Zum Beispiel kann im Falle des Lesers die [Symbol.dispose]-Eigenschaft ein einfacher Alias oder Wrapper von releaseLock sein:

// For demonstration
class MyReader {
  // A wrapper
  [Symbol.dispose]() {
    this.releaseLock();
  }
  releaseLock() {
    // Logic to release resources
  }
}

// OR, an alias
MyReader.prototype[Symbol.dispose] = MyReader.prototype.releaseLock;

Durch das disposable-Protokoll kann using alle Ressourcen konsistent freigeben, ohne zu wissen, welcher Ressourcentyp es ist.

Jeder Gültigkeitsbereich hat eine Liste von Ressourcen, die mit ihm verbunden sind, in der Reihenfolge, in der sie deklariert wurden. Wenn der Bereich verlassen wird, werden die Ressourcen in umgekehrter Reihenfolge freigegeben, indem ihre [Symbol.dispose]()-Methode aufgerufen wird. Zum Beispiel wird im obigen Beispiel reader1 vor reader2 deklariert, also wird reader2 zuerst und dann reader1 freigegeben. Fehler, die beim Versuch, eine Ressource freizugeben, ausgelöst werden, verhindern nicht die Freigabe anderer Ressourcen. Dies ist konsistent mit dem try...finally-Muster und berücksichtigt mögliche Abhängigkeiten zwischen den Ressourcen.

await using ist using sehr ähnlich. Die Syntax zeigt an, dass irgendwo ein await passiert – nicht wenn die Ressource deklariert wird, sondern tatsächlich wenn sie freigegeben wird. await using erfordert, dass die Ressource asynchron freigebbar ist, was bedeutet, dass sie eine [Symbol.asyncDispose]() Methode hat. Diese Methode wird ohne Argumente aufgerufen und gibt ein Promise zurück, das sich löst, wenn die Bereinigung abgeschlossen ist. Dies ist nützlich, wenn die Bereinigung asynchron ist, wie fileHandle.close(), in welchem Fall das Ergebnis der Freigabe nur asynchron bekannt sein kann.

{
  await using fileHandle = open("file.txt", "w");
  await fileHandle.write("Hello");

  // fileHandle.close() is called and awaited
}

Weil await using ein await erfordert, ist es nur in Kontexten erlaubt, in denen await ist, was async-Funktionen und Top-Level-awaits in Modulen einschließt.

Ressourcen werden sequentiell und nicht gleichzeitig bereinigt: Der Rückgabewert der [Symbol.asyncDispose]()-Methode einer Ressource wird vor dem Aufruf der nächsten [Symbol.asyncDispose]()-Methode erwartet.

Einige Punkte, die zu beachten sind:

Die DisposableStack- und AsyncDisposableStack-Objekte

using und await using sind spezielle Syntaxen. Syntaxen sind praktisch und verbergen viel der Komplexität, aber manchmal müssen Sie Dinge manuell tun.

Ein häufiges Beispiel: Was, wenn Sie die Ressource nicht am Ende dieses Bereichs freigeben möchten, sondern in einem späteren Bereich? Betrachten Sie dies:

let reader;
if (someCondition) {
  reader = stream.getReader();
} else {
  reader = stream.getReader({ mode: "byob" });
}

Wie gesagt, using ist wie const: Es muss initialisiert sein und kann nicht neu zugewiesen werden, sodass Sie dies versuchen könnten:

if (someCondition) {
  using reader = stream.getReader();
} else {
  using reader = stream.getReader({ mode: "byob" });
}

Dies bedeutet jedoch, dass die gesamte Logik innerhalb des if oder else geschrieben werden muss, was zu viel Duplikation führt. Was wir tun möchten, ist, die Ressource in einem Bereich zu erwerben und sie in einem anderen freizugeben. Wir können dafür einen DisposableStack verwenden, der ein Objekt ist, das eine Sammlung von disposablen Ressourcen hält und selbst disposable ist:

{
  using disposer = new DisposableStack();
  let reader;
  if (someCondition) {
    reader = disposer.use(stream.getReader());
  } else {
    reader = disposer.use(stream.getReader({ mode: "byob" }));
  }
  // Do something with reader
  // Before scope exit, disposer is disposed, which disposes reader
}

Sie könnten eine Ressource haben, die das disposable-Protokoll noch nicht implementiert, sodass sie von using abgelehnt wird. In diesem Fall können Sie adopt() verwenden.

{
  using disposer = new DisposableStack();
  // Suppose reader does not have the [Symbol.dispose]() method,
  // then it cannot be used with using.
  // However, we can manually pass a disposer function to disposer.adopt
  const reader = disposer.adopt(stream.getReader(), (reader) =>
    reader.releaseLock(),
  );
  // Do something with reader
  // Before scope exit, disposer is disposed, which disposes reader
}

Sie könnten eine Bereinigungsaktion ausführen müssen, die nicht mit einer bestimmten Ressource "verbunden" ist. Vielleicht möchten Sie einfach nur eine Nachricht wie "Alle Datenbankverbindungen geschlossen" protokollieren, wenn mehrere Verbindungen gleichzeitig geöffnet sind. In diesem Fall können Sie defer() verwenden.

{
  using disposer = new DisposableStack();
  disposer.defer(() => console.log("All database connections closed"));
  const connection1 = disposer.use(openConnection());
  const connection2 = disposer.use(openConnection());
  // Do something with connection1 and connection2
  // Before scope exit, disposer is disposed, which first disposes connection1
  // and connection2 and then logs the message
}

Sie möchten möglicherweise eine bedingte Freigabe durchführen – zum Beispiel nur erworbene Ressourcen freigeben, wenn ein Fehler aufgetreten ist. In diesem Fall können Sie move() verwenden, um die Ressourcen zu bewahren, die ansonsten freigegeben würden.

class MyResource {
  #resource1;
  #resource2;
  #disposables;
  constructor() {
    using disposer = new DisposableStack();
    this.#resource1 = disposer.use(getResource1());
    this.#resource2 = disposer.use(getResource2());
    // If we made it here, then there were no errors during construction and
    // we can safely move the disposables out of `disposer` and into `#disposables`.
    this.#disposables = disposer.move();
    // If construction failed, then `disposer` would be disposed before reaching
    // the line above, disposing `#resource1` and `#resource2`.
  }
  [Symbol.dispose]() {
    this.#disposables.dispose(); // Dispose `#resource2` and `#resource1`.
  }
}

AsyncDisposableStack ist wie DisposableStack, aber zur Verwendung mit asynchronen disposable Ressourcen. Seine use()-Methode erwartet ein async disposable, seine adopt()-Methode erwartet eine asynchrone Bereinigungsfunktion und seine dispose()-Methode erwartet einen asynchronen Callback. Es bietet eine [Symbol.asyncDispose]() Methode. Sie können ihm weiterhin synchrone Ressourcen übergeben, wenn Sie einen Mix aus synchronen und asynchronen Ressourcen haben.

Das Referenzdokument für DisposableStack enthält mehr Beispiele und Details.

Fehlerbehandlung

Ein Hauptanwendungsfall der Ressourcenmanagementfunktion ist sicherzustellen, dass Ressourcen immer freigegeben werden, selbst wenn ein Fehler auftritt. Lassen Sie uns einige komplexe Fehlerbehandlungsszenarien untersuchen.

Wir beginnen mit dem folgenden Code, der durch die Verwendung von using gegen Fehler robust ist:

async function readUntil(stream, text) {
  // Use `using` instead of `await using` because `releaseLock` is synchronous
  using reader = stream.getReader();
  let chunk = await reader.read();

  while (!chunk.done && chunk.value !== text) {
    console.log(chunk.toUpperCase());
    chunk = await reader.read();
  }
}

Angenommen, chunk stellte sich als null heraus. Dann wird toUpperCase() einen TypeError auslösen, was dazu führt, dass die Funktion beendet wird. Bevor die Funktion beendet wird, wird stream[Symbol.dispose]() aufgerufen, was das Sperren des Streams aufhebt.

const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("a");
    controller.enqueue(null);
    controller.enqueue("b");
    controller.enqueue("c");
    controller.close();
  },
});

readUntil(stream, "b")
  .catch((e) => console.error(e)) // TypeError: chunk.toUpperCase is not a function
  .then(() => {
    const anotherReader = stream.getReader();
    // Successfully creates another reader
  });

Daher unterdrückt using keine Fehler: Alle auftretenden Fehler werden weiterhin geworfen, aber die Ressourcen werden direkt vorher geschlossen. Was passiert nun, wenn die Ressourcensäuberung selbst einen Fehler wirft? Lassen Sie uns ein noch konstruierteres Beispiel verwenden:

class MyReader {
  [Symbol.dispose]() {
    throw new Error("Failed to release lock");
  }
}

function doSomething() {
  using reader = new MyReader();
  throw new Error("Failed to read");
}

try {
  doSomething();
} catch (e) {
  console.error(e); // SuppressedError: An error was suppressed during disposal
}

In dem Aufruf von doSomething() werden zwei Fehler generiert: ein Fehler, der während doSomething ausgelöst wird, und ein Fehler, der beim Freigeben von reader aufgrund des ersten Fehlers ausgelöst wird. Beide Fehler werden zusammen geworfen, sodass das, was Sie gefangen haben, ein SuppressedError ist. Dies ist ein spezieller Fehler, der zwei Fehler umschließt: Die error-Eigenschaft enthält den späteren Fehler, und die suppressed-Eigenschaft enthält den früheren Fehler, der durch den späteren Fehler "unterdrückt" wird.

Wenn wir mehr als eine Ressource haben und beide von ihnen während der Freigabe einen Fehler auslösen (dies sollte äußerst selten sein – es ist bereits selten, dass die Freigabe fehlschlägt!), dann wird jeder frühere Fehler durch den späteren Fehler unterdrückt und bildet eine Kette unterdrückter Fehler.

class MyReader {
  [Symbol.dispose]() {
    throw new Error("Failed to release lock on reader");
  }
}

class MyWriter {
  [Symbol.dispose]() {
    throw new Error("Failed to release lock on writer");
  }
}

function doSomething() {
  using reader = new MyReader();
  using writer = new MyWriter();
  throw new Error("Failed to read");
}

try {
  doSomething();
} catch (e) {
  console.error(e); // SuppressedError: An error was suppressed during disposal
  console.error(e.suppressed); // SuppressedError: An error was suppressed during disposal
  console.error(e.error); // Error: Failed to release lock on reader
  console.error(e.suppressed.suppressed); // Error: Failed to read
  console.error(e.suppressed.error); // Error: Failed to release lock on writer
}
Beispiele Automatisches Freigeben von Objekt-URLs

Im folgenden Beispiel erstellen wir eine Objekt-URL zu einem Blob (in einer realen Anwendung würde dieser Blob von irgendwoher abgerufen, wie einer Datei oder einer Abruffunktion), damit wir den Blob als Datei herunterladen können. Um ein Ressourcenleck zu vermeiden, müssen wir die Objekt-URL mithilfe von URL.revokeObjectURL() freigeben, sobald sie nicht mehr benötigt wird (das heißt, wenn der Download erfolgreich gestartet wurde). Da die URL selbst nur eine Zeichenfolge ist und daher nicht das disposable-Protokoll implementiert, können wir url nicht direkt mit using deklarieren; daher erstellen wir einen DisposableStack, der als Freigeber für url dient. Die Objekt-URL wird sofort gelöscht, wenn disposer aus dem Gültigkeitsbereich geht, was entweder dann der Fall ist, wenn link.click() abgeschlossen ist oder ein Fehler auftritt.

const downloadButton = document.getElementById("download-button");
const exampleBlob = new Blob(["example data"]);

downloadButton.addEventListener("click", () => {
  using disposer = new DisposableStack();
  const link = document.createElement("a");
  const url = disposer.adopt(
    URL.createObjectURL(exampleBlob),
    URL.revokeObjectURL,
  );

  link.href = url;
  link.download = "example.txt";
  link.click();
});
Automatisches Abbrechen von laufenden Anfragen

Im folgenden Beispiel rufen wir eine Liste von Ressourcen gleichzeitig mit Promise.all() ab. Promise.all() schlägt fehl und weist das resultierende Promise zurück, sobald eine Anfrage fehlgeschlagen ist; jedoch laufen die anderen ausstehenden Anfragen weiter, obwohl ihre Ergebnisse für das Programm unzugänglich sind. Um zu verhindern, dass diese verbleibenden Anfragen unnötig Ressourcen verbrauchen, müssen wir laufende Anfragen automatisch stornieren, sobald Promise.all() abgeschlossen ist. Wir implementieren die Stornierung mit einem AbortController und übergeben dessen signal an jeden fetch()-Aufruf. Wenn Promise.all() erfolgreich ist, gibt die Funktion normal zurück und der Controller bricht ab, was harmlos ist, da keine ausstehende Anfrage zu stornieren ist; wenn Promise.all() abgelehnt wird und die Funktion einen Fehler wirft, bricht der Controller ab und storniert alle ausstehenden Anfragen.

async function getAllData(urls) {
  using disposer = new DisposableStack();
  const { signal } = disposer.adopt(new AbortController(), (controller) =>
    controller.abort(),
  );

  // Fetch all URLs in parallel
  // Automatically cancel any incomplete requests if any request fails
  const pages = await Promise.all(
    urls.map((url) =>
      fetch(url, { signal }).then((response) => {
        if (!response.ok)
          throw new Error(
            `Response error: ${response.status} - ${response.statusText}`,
          );
        return response.text();
      }),
    ),
  );
  return pages;
}
Fallstricke

Die Ressourcensäuberungssyntax bietet viele starke Fehlerbehandlungsgarantien, die sicherstellen, dass die Ressourcen aufgeräumt werden, egal was passiert, aber es gibt einige Fallstricke, die Sie dennoch erleben könnten:

Die Ressourcenmanagementfunktion ist kein Allheilmittel. Sie stellt definitiv eine Verbesserung gegenüber dem manuellen Aufrufen der Freigabemethoden dar, aber sie ist nicht smart genug, um alle Ressourcenmanagementfehler zu verhindern. Sie müssen dennoch vorsichtig sein und die Semantik der Ressourcen, die Sie verwenden, verstehen.

Fazit

Hier sind die wichtigsten Komponenten des Ressourcenmanagementsystems:

Mit der ordnungsgemäßen Nutzung dieser APIs können Sie Systeme erstellen, die mit externen Ressourcen interagieren und stark und robust gegen alle Fehlerbedingungen bleiben, ohne viel Boilerplate-Code.


RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4