Sie müssen Objekte in JS manuell "zerstören". Das Erstellen einer Zerstörungsfunktion ist in JS üblich. In anderen Sprachen kann dies als "frei", "freigeben", "entsorgen", "schließen" usw. bezeichnet werden. Nach meiner Erfahrung handelt es sich jedoch tendenziell um "Zerstören", wodurch interne Verweise, Ereignisse und möglicherweise auch zerstörte Zerstörungsaufrufe an untergeordnete Objekte aufgehoben werden.
WeakMaps sind weitgehend nutzlos, da sie nicht iteriert werden können und wahrscheinlich erst nach ECMA 7 verfügbar sind, wenn überhaupt. Mit WeakMaps können Sie nur unsichtbare Eigenschaften vom Objekt selbst trennen, mit Ausnahme der Suche nach Objektreferenz und GC, damit sie es nicht stören. Dies kann zum Zwischenspeichern, Erweitern und Behandeln von Pluralität nützlich sein, hilft jedoch nicht wirklich bei der Speicherverwaltung für Observablen und Beobachter. WeakSet ist eine Teilmenge von WeakMap (wie eine WeakMap mit dem Standardwert boolean true).
Es gibt verschiedene Argumente, ob verschiedene Implementierungen von schwachen Referenzen für diese oder Destruktoren verwendet werden sollen. Beide haben potenzielle Probleme und Destruktoren sind begrenzter.
Destruktoren sind möglicherweise auch für Beobachter / Zuhörer potenziell nutzlos, da der Zuhörer normalerweise entweder direkt oder indirekt Verweise auf den Beobachter enthält. Ein Destruktor arbeitet wirklich nur als Proxy ohne schwache Referenzen. Wenn Ihr Observer wirklich nur ein Proxy ist, der die Listener eines anderen nimmt und sie auf ein Observable setzt, kann er dort etwas tun, aber so etwas ist selten nützlich. Destruktoren sind eher für E / A-bezogene Dinge oder für Dinge außerhalb des Umfangs des Containments gedacht (IE, der zwei von ihm erstellte Instanzen miteinander verbindet).
Der spezielle Fall, nach dem ich angefangen habe, ist, weil ich eine Klasse-A-Instanz habe, die Klasse B im Konstruktor nimmt und dann eine Klasse-C-Instanz erstellt, die auf B hört. Ich behalte die B-Instanz immer irgendwo oben. KI wird manchmal weggeworfen, neue erstellt, viele erstellt usw. In dieser Situation würde ein Destruktor tatsächlich für mich arbeiten, aber mit einem bösen Nebeneffekt, der im übergeordneten Element, wenn ich die C-Instanz weitergab, aber alle A-Referenzen entfernte, dann die C- und Die B-Bindung würde unterbrochen (bei C wird der Boden darunter entfernt).
In JS ist es schmerzhaft, keine automatische Lösung zu haben, aber ich denke nicht, dass es leicht lösbar ist. Betrachten Sie diese Klassen (Pseudo):
function Filter(stream) {
stream.on('data', function() {
this.emit('data', data.toString().replace('somenoise', ''));
});
}
Filter.prototype.__proto__ = EventEmitter.prototype;
function View(df, stream) {
df.on('data', function(data) {
stream.write(data.toUpper());
});
}
Nebenbei bemerkt ist es schwierig, Dinge ohne anonyme / eindeutige Funktionen zum Laufen zu bringen, die später behandelt werden.
Im Normalfall wäre die Instanziierung wie folgt (Pseudo):
var df = new Filter(stdin),
v1 = new View(df, stdout),
v2 = new View(df, stderr);
Um diese normalerweise zu GCen, würden Sie sie auf null setzen, aber es wird nicht funktionieren, da sie einen Baum mit stdin im Stamm erstellt haben. Dies ist im Grunde das, was Ereignissysteme tun. Sie geben einem Kind ein Elternteil, das Kind fügt sich dem Elternteil hinzu und kann dann einen Verweis auf das Elternteil beibehalten oder nicht. Ein Baum ist ein einfaches Beispiel, aber in Wirklichkeit finden Sie sich möglicherweise auch mit komplexen Graphen wieder, wenn auch selten.
In diesem Fall fügt Filter stdin einen Verweis auf sich selbst in Form einer anonymen Funktion hinzu, die indirekt auf Filter nach Bereich verweist. Bereichsreferenzen sind etwas zu beachten und das kann sehr komplex sein. Ein leistungsstarker GC kann einige interessante Dinge tun, um Elemente in Bereichsvariablen zu entfernen, aber das ist ein anderes Thema. Es ist wichtig zu verstehen, dass, wenn Sie eine anonyme Funktion erstellen und sie als Listener zu ab beobachtbar zu etwas hinzufügen, die beobachtbare Funktion einen Verweis auf die Funktion und alles behält, worauf die Funktion in den darüber liegenden Bereichen verweist (in denen sie definiert wurde) ) wird ebenfalls beibehalten. Die Ansichten machen dasselbe, aber nach der Ausführung ihrer Konstruktoren behalten die Kinder keinen Verweis auf ihre Eltern bei.
Wenn ich einige oder alle der oben deklarierten Variablen auf null setze, wird dies keinen Unterschied machen (ähnlich, wenn dieser "Haupt" -Bereich beendet ist). Sie sind weiterhin aktiv und leiten Daten von stdin nach stdout und stderr weiter.
Wenn ich sie alle auf null setze, ist es unmöglich, sie zu entfernen oder zu GCen, ohne die Ereignisse auf stdin zu löschen oder stdin auf null zu setzen (vorausgesetzt, es kann so freigegeben werden). Sie haben im Grunde genommen einen Speicherverlust auf diese Weise mit tatsächlich verwaisten Objekten, wenn der Rest des Codes stdin benötigt und andere wichtige Ereignisse darauf sind, die Sie daran hindern, die oben genannten Aktionen auszuführen.
Um df, v1 und v2 loszuwerden, muss ich für jeden eine Zerstörungsmethode aufrufen. In Bezug auf die Implementierung bedeutet dies, dass sowohl die Filter- als auch die View-Methode den Verweis auf die von ihnen erstellte anonyme Listener-Funktion sowie die Observable beibehalten und diese an removeListener übergeben müssen.
Nebenbei bemerkt, Sie können alternativ ein Observable haben, das einen Index zurückgibt, um die Listener zu verfolgen, sodass Sie prototypisierte Funktionen hinzufügen können, die zumindest nach meinem Verständnis in Bezug auf Leistung und Speicher viel besser sein sollten. Sie müssen jedoch den zurückgegebenen Bezeichner nachverfolgen und Ihr Objekt übergeben, um sicherzustellen, dass der Listener beim Aufruf daran gebunden ist.
Eine Zerstörungsfunktion fügt mehrere Schmerzen hinzu. Erstens müsste ich es aufrufen und die Referenz freigeben:
df.destroy();
v1.destroy();
v2.destroy();
df = v1 = v2 = null;
Dies ist ein kleiner Ärger, da es etwas mehr Code ist, aber das ist nicht das eigentliche Problem. Wenn ich diese Referenzen an viele Objekte weitergebe. Wann genau nennst du in diesem Fall zerstören? Sie können diese nicht einfach an andere Objekte weitergeben. Sie werden mit Ketten von Zerstörungen und manueller Implementierung der Verfolgung entweder durch Programmablauf oder auf andere Weise enden. Du kannst nicht schießen und vergessen.
Ein Beispiel für diese Art von Problem ist, wenn ich beschließe, dass View bei Zerstörung auch destroy auf df aufruft. Wenn v2 immer noch in der Nähe ist, wird die Zerstörung von df unterbrochen, sodass die Zerstörung nicht einfach an df weitergeleitet werden kann. Wenn v1 df benötigt, um es zu verwenden, müsste es df mitteilen, dass es verwendet wird, was einen Zähler oder ähnliches wie df auslösen würde. Die Zerstörungsfunktion von df würde abnehmen als der Zähler und nur dann tatsächlich zerstören, wenn sie 0 ist. Diese Art von Dingen fügt eine Menge Komplexität hinzu und fügt eine Menge hinzu, die schief gehen können. Die offensichtlichste davon ist, etwas zu zerstören, während es irgendwo noch einen Hinweis darauf gibt wird verwendet und Zirkelverweise (an diesem Punkt geht es nicht mehr darum, einen Zähler zu verwalten, sondern eine Karte der referenzierenden Objekte). Wenn Sie daran denken, Ihre eigenen Referenzzähler, MM usw. in JS zu implementieren, dann ist es '
Wenn WeakSets iterierbar wären, könnte dies verwendet werden:
function Observable() {
this.events = {open: new WeakSet(), close: new WeakSet()};
}
Observable.prototype.on = function(type, f) {
this.events[type].add(f);
};
Observable.prototype.emit = function(type, ...args) {
this.events[type].forEach(f => f(...args));
};
Observable.prototype.off = function(type, f) {
this.events[type].delete(f);
};
In diesem Fall muss die besitzende Klasse auch einen Token-Verweis auf f behalten, sonst geht es schief.
Wenn Observable anstelle von EventListener verwendet würde, würde die Speicherverwaltung in Bezug auf die Ereignis-Listener automatisch erfolgen.
Anstatt "destroy" für jedes Objekt aufzurufen, würde dies ausreichen, um sie vollständig zu entfernen:
df = v1 = v2 = null;
Wenn Sie df nicht auf null setzen würden, wäre es immer noch vorhanden, aber v1 und v2 würden automatisch ausgehängt.
Bei diesem Ansatz gibt es jedoch zwei Probleme.
Problem eins ist, dass es eine neue Komplexität hinzufügt. Manchmal wollen die Leute dieses Verhalten eigentlich nicht. Ich könnte eine sehr große Kette von Objekten erstellen, die eher durch Ereignisse als durch Eindämmung miteinander verbunden sind (Verweise in Konstruktorbereichen oder Objekteigenschaften). Irgendwann müssten ein Baum und ich nur noch um die Wurzel herumgehen und uns darüber Sorgen machen. Das Befreien der Wurzel würde das Ganze bequem befreien. Beide Verhaltensweisen sind abhängig vom Codierungsstil usw. nützlich. Wenn Sie wiederverwendbare Objekte erstellen, ist es schwierig zu wissen, was die Benutzer wollen, was sie getan haben, was Sie getan haben und wie schwierig es ist, das zu umgehen, was getan wurde. Wenn ich Observable anstelle von EventListener verwende, muss df entweder auf v1 und v2 verweisen, oder ich muss sie alle übergeben, wenn ich das Eigentum an der Referenz auf etwas anderes außerhalb des Gültigkeitsbereichs übertragen möchte. Eine schwache Referenz würde das Problem ein wenig abmildern, indem die Kontrolle von Observable auf einen Beobachter übertragen wird, sie jedoch nicht vollständig lösen (und jede Emission oder jedes Ereignis auf sich selbst überprüfen müssen). Ich nehme an, dieses Problem kann behoben werden, wenn das Verhalten nur für isolierte Diagramme gilt, die den GC erheblich komplizieren würden, und nicht für Fälle, in denen es Referenzen außerhalb des Diagramms gibt, die in der Praxis keine Probleme sind (nur CPU-Zyklen verbrauchen, keine Änderungen vorgenommen).
Problem zwei ist, dass es entweder in bestimmten Fällen unvorhersehbar ist oder die JS-Engine zwingt, das GC-Diagramm für die Objekte bei Bedarf zu durchlaufen, was einen schrecklichen Einfluss auf die Leistung haben kann (obwohl es, wenn es klug ist, vermeiden kann, es pro Mitglied zu tun, indem es es pro macht WeakMap-Schleife stattdessen). Der GC wird möglicherweise nie ausgeführt, wenn die Speichernutzung einen bestimmten Schwellenwert nicht erreicht und das Objekt mit seinen Ereignissen nicht entfernt wird. Wenn ich v1 auf null setze, wird es möglicherweise für immer an stdout weitergeleitet. Selbst wenn es GCed wird, ist dies willkürlich, es kann für eine beliebige Zeitspanne (1 Zeilen, 10 Zeilen, 2,5 Zeilen usw.) an stdout weitergeleitet werden.
Der Grund, warum WeakMap sich nicht um den GC kümmert, wenn er nicht iterierbar ist, besteht darin, dass Sie für den Zugriff auf ein Objekt ohnehin einen Verweis darauf haben müssen, damit es entweder nicht GCed ist oder nicht zur Karte hinzugefügt wurde.
Ich bin mir nicht sicher, was ich von so etwas halte. Sie brechen die Speicherverwaltung, um sie mit dem iterierbaren WeakMap-Ansatz zu beheben. Problem zwei kann auch für Destruktoren existieren.
All dies ruft mehrere Ebenen der Hölle hervor, daher würde ich vorschlagen, zu versuchen, es mit gutem Programmdesign, guten Praktiken, Vermeidung bestimmter Dinge usw. zu umgehen. Es kann in JS jedoch frustrierend sein, weil es in bestimmten Aspekten flexibel ist und weil Es ist natürlicher asynchron und ereignisbasiert mit starker Umkehrung der Kontrolle.
Es gibt eine andere Lösung, die ziemlich elegant ist, aber auch hier noch einige potenziell schwerwiegende Probleme aufweist. Wenn Sie eine Klasse haben, die eine beobachtbare Klasse erweitert, können Sie die Ereignisfunktionen überschreiben. Fügen Sie Ihre Ereignisse nur dann zu anderen Observablen hinzu, wenn Ereignisse zu sich selbst hinzugefügt werden. Wenn alle Ereignisse von Ihnen entfernt wurden, entfernen Sie Ihre Ereignisse von untergeordneten Elementen. Sie können auch eine Klasse erstellen, um Ihre beobachtbare Klasse zu erweitern und dies für Sie zu tun. Eine solche Klasse könnte Hooks für leer und nicht leer bereitstellen, so dass Sie sich selbst beobachten würden. Dieser Ansatz ist nicht schlecht, hat aber auch Probleme. Es gibt eine Zunahme der Komplexität sowie eine Abnahme der Leistung. Sie müssen einen Verweis auf das Objekt behalten, das Sie beobachten. Kritisch ist, dass es auch bei Blättern nicht funktioniert, aber zumindest die Zwischenprodukte zerstören sich selbst, wenn Sie das Blatt zerstören. Es' s wie Verketten zerstören, aber versteckt hinter Anrufen, die Sie bereits verketten müssen. Ein großes Leistungsproblem besteht darin, dass Sie möglicherweise interne Daten aus dem Observable jedes Mal neu initialisieren müssen, wenn Ihre Klasse aktiv wird. Wenn dieser Vorgang sehr lange dauert, sind Sie möglicherweise in Schwierigkeiten.
Wenn Sie WeakMap iterieren könnten, könnten Sie möglicherweise Dinge kombinieren (zu Schwach wechseln, wenn keine Ereignisse vorliegen, Stark, wenn Ereignisse), aber alles, was wirklich getan wird, ist, das Leistungsproblem auf eine andere Person zu übertragen.
Es gibt auch sofortige Belästigungen mit iterierbarer WeakMap, wenn es um Verhalten geht. Ich habe kurz zuvor über Funktionen mit Bereichsreferenzen und Schnitzen gesprochen. Wenn ich ein untergeordnetes Element instanziiere, das im Konstruktor den Listener 'console.log (param)' mit dem übergeordneten Element verknüpft und das übergeordnete Element nicht beibehalten kann, kann es beim Entfernen aller Verweise auf das untergeordnete Element vollständig als anonyme Funktion freigegeben werden, die dem übergeordneten Element hinzugefügt wird Eltern verweisen auf nichts innerhalb des Kindes. Dies lässt die Frage offen, was mit parent.weakmap.add (child, (param) => console.log (param)) zu tun ist. Meines Wissens ist der Schlüssel schwach, aber nicht der Wert, so dass schwachmap.add (Objekt, Objekt) persistent ist. Dies ist jedoch etwas, das ich neu bewerten muss. Für mich sieht das wie ein Speicherverlust aus, wenn ich alle anderen Objektreferenzen entsorge, aber ich vermute, dass dies in Wirklichkeit gelingt, indem es als Zirkelreferenz betrachtet wird. Entweder behält die anonyme Funktion einen impliziten Verweis auf Objekte bei, die sich aus übergeordneten Bereichen ergeben, um eine Konsistenz zu erzielen, die viel Speicher verschwendet, oder Sie haben ein Verhalten, das aufgrund von Umständen variiert, die schwer vorherzusagen oder zu verwalten sind. Ich denke, Ersteres ist eigentlich unmöglich. Im letzteren Fall, wenn ich eine Methode für eine Klasse habe, die einfach ein Objekt nimmt und console.log hinzufügt, wird sie freigegeben, wenn ich die Verweise auf die Klasse lösche, selbst wenn ich die Funktion zurückgebe und eine Referenz verwalte.