Get to know MDN better
Dieser Inhalt wurde automatisch aus dem Englischen übersetzt, und kann Fehler enthalten. Erfahre mehr über dieses Experiment.
Dieser Leitfaden behandelt, wie man in JavaScript Ressourcenverwaltung durchführt. Ressourcenverwaltung ist nicht genau dasselbe wie Speicherverwaltung, welches ein fortgeschritteneres Thema ist und normalerweise automatisch von JavaScript gehandhabt wird. Ressourcenverwaltung dreht sich um 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 in die Anwendungslogik eingreifen, aber Ressourcenlecks führen oft dazu, dass Dinge nicht funktionieren oder der Speicherverbrauch stark zunimmt. Daher ist dies keine optionale Optimierungsfunktion, sondern eine grundlegende Funktion zum Schreiben korrekter Programme!
Hinweis: Auch wenn Speicherverwaltung und Ressourcenverwaltung zwei getrennte Themen sind, können Sie manchmal als letzte Möglichkeit in das Speichermanagementsystem eingreifen, um Ressourcenverwaltung durchzuführen. Wenn Sie beispielsweise über ein JavaScript-Objekt verfügen, 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 keine Möglichkeit gibt, auf die Ressource danach zuzugreifen. Es gibt jedoch keine Garantie, dass der Finalizer ausgeführt wird, daher ist es keine gute Idee, sich für kritische Ressourcen darauf zu verlassen.
Betrachten wir zunächst einige Beispiele von Ressourcen, die verwaltet werden müssen:
Datei-Handles: Ein Datei-Handle wird verwendet, um Bytes in einer Datei zu lesen und zu schreiben. Wenn Sie damit fertig sind, müssen Sie fileHandle.close() aufrufen, andernfalls bleibt die Datei offen, selbst wenn das JS-Objekt nicht mehr zugänglich ist. Wie in der verlinkten Node.js-Dokumentation erwähnt:
Wenn ein <FileHandle> nicht mittels der fileHandle.close()-Methode geschlossen wird, versucht es, den Dateideskriptor automatisch zu schließen und eine Prozesswarnung auszugeben, um Speicherlecks zu verhindern. Bitte verlassen Sie sich nicht auf dieses Verhalten, da es unzuverlässig sein kann und die Datei möglicherweise nicht geschlossen wird. Schließen Sie <FileHandle>s stattdessen immer explizit. Node.js kann dieses Verhalten in Zukunft ändern.
Netzwerkverbindungen: Einige Verbindungen, wie WebSocket und RTCPeerConnection, müssen geschlossen werden, wenn keine Nachrichten übertragen werden. Andernfalls bleibt die Verbindung offen, und Verbindungspools sind oft sehr begrenzt in ihrer Größe.
Stream-Leser: Wenn Sie ReadableStreamDefaultReader.releaseLock() nicht aufrufen, wird der Stream gesperrt und erlaubt es einem anderen Leser nicht, ihn zu konsumieren.
Hier ist ein konkretes Beispiel, das einen lesbaren Stream verwendet:
Hier haben wir einen Stream, der drei Datenstücke ausgibt. Wir lesen vom Stream, bis wir den Buchstaben "b" finden. Wenn readUntil zurückkehrt, ist der Stream nur teilweise verbraucht, so dass wir in der Lage sein sollten, weiterhin mit einem anderen Leser daraus zu lesen. Wir haben jedoch vergessen, die Sperre freizugeben, sodass der Stream noch gesperrt ist und wir keinen weiteren Leser erstellen können, obwohl reader nicht mehr verfügbar ist.
Die Lösung in diesem Fall ist einfach: Rufen Sie reader.releaseLock() am Ende von readUntil auf. Aber einige Probleme bleiben bestehen:
Inkonsistenz: Verschiedene Ressourcen haben verschiedene Möglichkeiten, sie freizugeben. Zum Beispiel haben wir close(), releaseLock(), disconnect() usw. Das Muster verallgemeinert sich nicht.
Fehlerbehandlung: Was passiert, wenn der Aufruf von reader.read() fehlschlägt? Dann würde readUntil enden und nie zum Aufruf von reader.releaseLock() gelangen. Wir können dies mit try...finally beheben:
Aber Sie müssen daran denken, dies jedes Mal zu tun, wenn Sie einige wichtige Ressourcen freigeben müssen.
Scoping: Im obigen Beispiel ist reader bereits geschlossen, wenn wir die try...finally-Anweisung verlassen, aber es ist weiterhin in seinem Gültigkeitsbereich verfügbar. Das bedeutet, dass Sie es möglicherweise versehentlich verwenden, nachdem es geschlossen wurde.
Mehrere Ressourcen: Wenn wir zwei Leser auf verschiedenen Streams haben, müssen wir daran denken, beide freizugeben. Dies ist ein respektabler Versuch, dies zu tun:
Dies führt jedoch zu weiteren Fehlerbehandlungsproblemen. Wenn stream2.getReader() eine Ausnahme auslöst, wird reader1 nicht freigegeben; wenn reader1.releaseLock() einen Fehler auslöst, wird reader2 nicht freigegeben. Dies bedeutet, dass wir tatsächlich jedes Ressourcenakquisitions-Freigabe-Paar in seiner eigenen try...finally-Anweisung einwickeln müssen:
Sie sehen, wie eine scheinbar harmlose Aufgabe des Aufrufs von releaseLock schnell zu verschachteltem Boilerplate-Code führen kann. Aus diesem Grund bietet JavaScript integrierte Sprachunterstützung für die Ressourcenverwaltung.
Die Lösung, die wir haben, sind zwei spezielle Arten von Variablendeklarationen: using und await using. Sie sind const ähnlich, aber sie geben die Ressource automatisch frei, wenn die Variable außer Gültigkeitsbereich gerät, solange die Ressource wegwerfbar ist. Anhand des gleichen Beispiels wie oben können wir es so umschreiben:
Hinweis: Zum Zeitpunkt des Schreibens implementiert ReadableStreamDefaultReader nicht das Wegwerfprotokoll. Dies ist ein hypothetisches Beispiel.
Beachten Sie zuerst die zusätzlichen geschweiften Klammern um den Code. Dies erstellt einen neuen Block Gültigkeitsbereich für die using-Deklarationen. Ressourcen, die mit using deklariert wurden, werden automatisch freigegeben, wenn sie außerhalb des Gültigkeitsbereichs von using gelangen, was in diesem Fall immer dann der Fall ist, wenn wir den Block verlassen, entweder weil alle Anweisungen ausgeführt wurden oder weil irgendwo ein Fehler oder return/break/continue vorliegt.
Das bedeutet, using kann nur in einem Gültigkeitsbereich verwendet werden, der eine klare Lebensdauer hat – nämlich kann es 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 nie freigegeben werden kann, wenn die Seite nie geladen wird. Sie können es jedoch auf der obersten Ebene eines Moduls verwenden, da der Modulbereich endet, wenn das Modul die Ausführung beendet.
Jetzt wissen wir, wann using aufräumt. Aber wie wird das gemacht? using erfordert, dass die Ressource das wegwerfbare Protokoll implementiert. Ein Objekt ist wegwerfbar, wenn es die Methode [Symbol.dispose]() besitzt. Diese Methode wird ohne Argumente aufgerufen, um eine Bereinigung durchzuführen. Im Falle des Lesers kann die [Symbol.dispose]-Eigenschaft ein einfacher Alias oder Wrapper von releaseLock sein:
Durch das Wegwerfprotokoll kann using alle Ressourcen auf konsistente Weise entsorgen, ohne zu verstehen, um welche Art von Ressource es sich handelt.
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 entsorgt, indem ihre Methode [Symbol.dispose]() aufgerufen wird. Zum Beispiel wird im obigen Beispiel reader1 vor reader2 deklariert, also wird reader2 zuerst, dann reader1 entsorgt. Fehler, die beim Versuch, eine Ressource zu entsorgen, auftreten, verhindern nicht die Entsorgung anderer Ressourcen. Dies ist konsistent mit dem try...finally-Muster und respektiert mögliche Abhängigkeiten zwischen den Ressourcen.
await using ist sehr ähnlich zu using. Die Syntax sagt Ihnen, dass ein await irgendwo passiert – nicht wenn die Ressource deklariert wird, sondern tatsächlich, wenn sie entsorgt wird. await using erfordert, dass die Ressource asynchron wegwerfbar ist, was bedeutet, dass sie eine Methode [Symbol.asyncDisposable]() hat. Diese Methode wird ohne Argumente aufgerufen und gibt ein Versprechen zurück, das aufgelöst wird, wenn die Bereinigung abgeschlossen ist. Dies ist nützlich, wenn die Bereinigung asynchron ist, wie fileHandle.close(), in welchem Fall das Ergebnis der Entsorgung nur asynchron bekannt sein kann.
Da await using erfordert, ein await auszuführen, ist es nur in Kontexten zulässig, in denen await ist, was innerhalb von async-Funktionen und auf oberer Ebene von await in Modulen enthalten ist.
Ressourcen werden nacheinander und nicht gleichzeitig aufgeräumt: Der Rückgabewert der [Symbol.asyncDispose]()-Methode einer Ressource wird awaitet, bevor die nächste Ressource mit ihrer [Symbol.asyncDispose]()-Methode aufgerufen wird.
Einige Dinge zu beachten:
using und await using sind spezielle Syntaxen. Syntaxen sind bequem und verbergen viel der Komplexität, aber manchmal müssen Sie Dinge manuell erledigen.
Ein häufiges Beispiel: Was, wenn Sie die Ressource nicht am Ende dieses Gültigkeitsbereichs entsorgen möchten, sondern in einem späteren Gültigkeitsbereich? Bedenken Sie dies:
Wie gesagt, using ist wie const: es muss initialisiert werden und kann nicht neu zugewiesen werden, also könnten Sie versuchen, dies zu tun:
Dies bedeutet jedoch, dass alle Logik innerhalb des if oder else geschrieben werden muss, was zu vielen Duplikaten führt. Was wir tun möchten, ist die Ressource in einem Gültigkeitsbereich zu erwerben und zu registrieren, aber sie in einem anderen zu entsorgen. Wir können einen DisposableStack für diesen Zweck verwenden, der ein Objekt ist, das eine Sammlung von wegwerfbaren Ressourcen enthält und selbst wegwerfbar ist:
Vielleicht haben Sie eine Ressource, die noch nicht das wegwerfbare Protokoll implementiert, sie wird also von using abgelehnt. In diesem Fall können Sie adopt() verwenden.
Vielleicht haben Sie eine Entsorgungsaktion auszuführen, aber sie ist nicht an eine bestimmte Ressource "gebunden". Vielleicht möchten Sie nur eine Nachricht protokollieren, die sagt "Alle Datenbankverbindungen geschlossen", wenn mehrere Verbindungen gleichzeitig geöffnet sind. In diesem Fall können Sie defer() verwenden.
Vielleicht möchten Sie eine bedingte Entsorgung durchführen – zum Beispiel nur beanspruchte Ressourcen entsorgen, wenn ein Fehler aufgetreten ist. In diesem Fall können Sie move() verwenden, um die Ressourcen zu erhalten, die ansonsten entsorgt würden.
AsyncDisposableStack ist wie DisposableStack, aber zur Verwendung mit asynchron wegwerfbaren Ressourcen. Seine use()-Methode erwartet einen asynchronen Wegwerfer, seine adopt()-Methode erwartet eine asynchrone Bereinigungsfunktion und seine dispose()-Methode erwartet einen asynchronen Rückruf. Es stellt eine [Symbol.asyncDispose]()-Methode bereit. Sie können ihm weiterhin synchrone Ressourcen übergeben, wenn Sie eine Mischung aus synchronen und asynchronen haben.
Die Referenz für DisposableStack enthält weitere Beispiele und Details.
Ein Hauptanwendungsfall der Ressourcenverwaltungsfunktion besteht darin, sicherzustellen, dass Ressourcen immer freigegeben werden, selbst wenn ein Fehler auftritt. Lassen Sie uns einige komplexe Fehlermanagementszenarien untersuchen.
Wir beginnen mit dem folgenden Code, der durch die Verwendung von using robust gegenüber Fehlern ist:
Angenommen, chunk stellt sich als null heraus. Dann wird !chunk.done einen TypeError auslösen, der dazu führt, dass die Funktion beendet wird. Bevor die Funktion beendet wird, wird stream[Symbol.dispose]() aufgerufen, was die Sperre auf den Stream freigibt.
Somit verschluckt using keine Fehler: Alle auftretenden Fehler werden weiterhin geworfen, aber die Ressourcen werden direkt vorher geschlossen. Was passiert nun, wenn die Ressourcenbereinigung selbst auch einen Fehler auslöst? Lassen Sie uns ein konstruiertes Beispiel verwenden:
Im doSomething()-Aufruf werden zwei Fehler generiert: ein Fehler, der während doSomething ausgelöst wird, und ein Fehler, der während der Entsorgung von reader aufgrund des ersten Fehlers ausgelöst wird. Beide Fehler werden gemeinsam geworfen, so dass das, was Sie abgefangen haben, ein SuppressedError ist. Dies ist ein spezieller Fehler, der zwei Fehler umhüllt: Die Eigenschaft error enthält den späteren Fehler, und die Eigenschaft suppressed 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 Entsorgung einen Fehler werfen (was äußerst selten sein sollte – es ist bereits selten, dass die Entsorgung fehlschlägt!), dann wird jeder frühere Fehler durch den späteren Fehler unterdrückt, wodurch eine Kette von unterdrückten Fehlern entsteht.
Im folgenden Beispiel erstellen wir eine Objekt-URL zu einem Blob (in einer realen Anwendung würde dieser Blob von irgendwoher abgerufen werden, z. B. eine Datei oder eine Fetch-Antwort), so dass wir den Blob als Datei herunterladen können. Um ein Ressourcenleck zu verhindern, müssen wir die Objekt-URL mit URL.revokeObjectURL() freigeben, wenn sie nicht mehr benötigt wird (das heißt, wenn der Download erfolgreich gestartet wurde). Da die URL selbst nur eine Zeichenkette ist und daher nicht das wegwerfbare Protokoll implementiert, können wir url nicht direkt mit using deklarieren; daher erstellen wir einen DisposableStack, um als Entsorger für url zu dienen. Die Objekt-URL wird freigegeben, sobald disposer außer Gültigkeitsbereich gerät, was entweder der Fall ist, wenn link.click() beendet ist oder irgendwo ein Fehler auftritt.
Im folgenden Beispiel verwenden wir fetch, um eine Liste von Ressourcen mit Hilfe von Promise.all() gleichzeitig abzurufen. Promise.all() schlägt fehl und lehnt das resultierende Versprechen ab, sobald eine Anfrage fehlgeschlagen ist; jedoch laufen die anderen ausstehenden Anfragen weiter, obwohl ihre Ergebnisse für das Programm nicht zugänglich sind. Um zu verhindern, dass diese verbleibenden Anfragen unnötig Ressourcen verbrauchen, müssen wir automatisch laufende Anfragen abbrechen, wann immer Promise.all() abgeschlossen wird. Wir implementieren die Stornierung mit einem AbortController und übergeben dessen signal jedem fetch()-Aufruf. Wenn Promise.all() erfüllt wird, gibt die Funktion normal zurück und der Controller bricht ab, was harmlos ist, da es keine ausstehende Anfrage zu stornieren gibt; wenn Promise.all() abgelehnt und die Funktion geworfen wird, bricht der Controller ab und storniert alle ausstehenden Anfragen.
Die Ressourcenerstellungssyntax bietet viele starke Fehlerbehandlungsgarantien, die sicherstellen, dass die Ressourcen immer aufgeräumt werden, egal was passiert, aber es gibt einige Fallstricke, auf die Sie dennoch stoßen könnten:
Die Ressourcenverwaltungsfunktion ist kein Allheilmittel. Sie ist definitiv eine Verbesserung gegenüber dem manuellen Aufruf der Entsorgungsmethoden, aber sie ist nicht intelligent genug, um alle Ressourcenverwaltungsfehler zu verhindern. Sie müssen dennoch vorsichtig sein und die Semantik der von Ihnen verwendeten Ressourcen verstehen.
Hier sind die Schlüsselelemente des Ressourcenverwaltungssystems:
Mit der richtigen Nutzung dieser APIs können Sie Systeme erstellen, die mit externen Ressourcen interagieren und unter allen Fehlerbedingungen robust bleiben, ohne dass viel Boilerplate-Code erforderlich ist.
Der Bauplan für ein besseres Internet.
Besuche die gemeinnützige Muttergesellschaft der Mozilla Corporation, die Mozilla Foundation.
Teile dieses Inhalts sind ©1998–2026 von einzelnen mozilla.org-Mitwirkenden. Inhalte sind verfügbar unter einer Creative-Commons-Lizenz.