您必须在JS中手动“销毁”对象。创建销毁函数在JS中很常见。在其他语言中,这可以称为“释放”,“释放”,“处置”,“关闭”等。以我的经验,尽管它倾向于被销毁,这将取消内部引用,事件并可能将销毁调用传播给子对象。
WeakMaps基本上是无用的,因为它们无法迭代,并且可能根本无法使用,直到ECMA 7。WeakMaps所要做的就是从对象本身分离出不可见的属性,除了通过对象引用和GC查找之外,这样它们就不会干扰它。这对于缓存,扩展和处理复数可能很有用,但对于可观察对象和观察者的内存管理并没有真正帮助。WeakSet是WeakMap的子集(例如默认值为boolean true的WeakMap)。
关于是否为此或析构函数使用弱引用的各种实现,存在各种争论。两者都有潜在的问题,并且破坏者更受限制。
析构函数实际上也可能对观察者/侦听器无用,因为通常侦听器将直接或间接持有对观察者的引用。析构函数仅在没有弱引用的情况下才真正以代理方式工作。如果您的观察者实际上只是一个代理,可以将其他监听器放在另一个可观察者上,那么它可以在那里做点什么,但是这种事情很少有用。析构函数更多地用于与IO相关的事情或超出包含范围的事情(IE,将它创建的两个实例链接起来)。
我开始研究的特定情况是因为我有一个A类实例,该实例在构造函数中使用B类,然后创建了侦听B的C类实例。我始终将B实例保持在较高的位置。AI有时会扔掉,创建新的,创建很多,等等。在这种情况下,析构函数实际上会为我工作,但有一个令人讨厌的副作用,即在父级中,如果我传递C实例但删除了所有A引用,则C和B绑定将被破坏(C从其下方移去了地面)。
在JS中,没有自动解决方案会很痛苦,但是我认为它不容易解决。考虑以下类(伪):
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());
});
}
附带说明一下,如果没有匿名/唯一功能(稍后将介绍),则很难使事情正常进行。
在正常情况下,实例化应为(伪):
var df = new Filter(stdin),
v1 = new View(df, stdout),
v2 = new View(df, stderr);
通常,要对它们进行GC,您可以将它们设置为null,但是它将不起作用,因为它们已经在根目录下创建了一个stdin树。这基本上是事件系统的工作。您给一个孩子的父母,孩子将自己添加到父母,然后可能会或可能不会维护对父母的引用。一棵树是一个简单的例子,但实际上,您可能会发现自己拥有复杂的图形,尽管很少。
在这种情况下,Filter以匿名函数的形式将对自身的引用添加到stdin中,该匿名函数按作用域间接引用Filter。范围引用是要注意的事情,可能会非常复杂。功能强大的GC可以做一些有趣的事情来消除范围变量中的项,但这是另一个主题。关键要理解的是,当您创建一个匿名函数并将其作为ab observable的侦听器添加到某个对象时,observable将维护对该函数以及函数在其上方范围中引用的任何内容的引用(该定义已在)也将得到维护。这些视图具有相同的功能,但是在执行其构造函数后,子级不会保留对其父级的引用。
如果我将上面声明的任何或所有var设置为null,则不会对任何内容产生影响(类似于完成“主要”范围时的情况)。它们仍将处于活动状态,并将数据从stdin传递到stdout和stderr。
如果我将它们全部设置为null,则不可能在不清除stdin上的事件或将stdin设置为null的情况下就将它们删除或GC,(假设可以这样释放它们)。如果代码的其余部分需要标准输入,并且其他重要事件禁止您执行上述操作,那么基本上是这样的内存泄漏,实际上是孤立对象。
为了摆脱df,v1和v2,我需要分别对它们调用一次destroy方法。在实现方面,这意味着Filter和View方法都需要保留对它们创建的匿名侦听器函数以及可观察到的引用的引用,并将其传递给removeListener。
附带说明一下,或者,您可以有一个obserable,它返回一个索引以跟踪侦听器,以便您可以添加原型函数,至少在我看来,这些函数应该在性能和内存上要好得多。但是,您仍然必须跟踪返回的标识符,并传递您的对象以确保侦听器在被调用时绑定到该标识符。
破坏功能会增加一些麻烦。首先,我必须调用它并释放引用:
df.destroy();
v1.destroy();
v2.destroy();
df = v1 = v2 = null;
这是一个小麻烦,因为它需要更多代码,但这并不是真正的问题。当我将这些引用传递给许多对象时。在这种情况下,您确切叫什么时候销毁?您不能简单地将这些交给其他对象。您将最终获得销毁链,并通过程序流程或其他方式手动执行跟踪。你无法解雇并忘记。
这种问题的一个示例是,如果我确定View在销毁时也会在df上调用destroy。如果v2仍然存在,则销毁df会破坏它,因此销毁不能简单地传递给df。相反,当v1使用df来使用它时,它将需要告诉df它被使用,这将引发一些计数器或类似于df的情况。df的destroy函数将比counter减小,并且只有在为0时才会真正销毁。这种事情增加了很多复杂性,并且增加了很多可能出错的地方,其中最明显的就是销毁某些东西,而周围仍然有一个引用。将使用循环引用(此时不再是管理计数器的情况,而是引用对象的映射)。当您考虑在JS中实现自己的参考计数器,MM等时,
如果WeakSet是可迭代的,则可以使用:
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);
};
在这种情况下,拥有类还必须保留对f的令牌引用,否则它将变得po琐。
如果使用Observable代替EventListener,则关于事件侦听器的内存管理将是自动的。
不必在每个对象上调用destroy即可完全删除它们:
df = v1 = v2 = null;
如果您未将df设置为null,则它仍然存在,但是v1和v2会自动取消连接。
但是,这种方法有两个问题。
问题之一是它增加了新的复杂性。有时人们实际上并不想要这种行为。我可以创建一个很大的对象链,这些对象通过事件而不是包含(构造函数作用域或对象属性中的引用)相互链接。最终,只有一棵树,我只需要绕过根部而不必担心。释放根将方便地释放整个东西。这两种行为都取决于编码样式等,它们都很有用,并且在创建可重用对象时,很难知道人们想要什么,他们做了什么,您做了什么以及为完成工作而苦恼。如果我使用Observable而不是EventListener,则df要么需要引用v1和v2,要么如果我想将引用的所有权转移到其他超出范围的对象,则必须全部传递它们。诸如此类的弱引用可以通过将控制权从Observable转移给观察者来减轻问题,但不能完全解决(需要检查自身的每个发射或事件)。我猜想,如果该行为仅适用于孤立的图,则该问题将得到解决,这会使GC严重复杂化,而不适用于图外实际上没有引用的引用(仅消耗CPU周期,不进行任何更改)的情况。
问题二是要么在某些情况下是不可预测的,要么强制JS引擎遍历那些按需使用的对象的GC图形,这可能会对性能产生可怕的影响(尽管它很聪明,但可以通过按每个对象执行操作来避免按成员执行操作改为使用WeakMap循环)。如果内存使用量未达到特定阈值且事件不会被删除,则GC可能永远不会运行。如果我将v1设置为null,它可能仍会永远中继到stdout。即使确实获得了GC,这也是任意的,它可能会继续中继到stdout任意时间(1行,10行,2.5行等)。
WeakMap在不可迭代时不关心GC的原因是,访问一个对象无论如何都必须对其进行引用,这样就不会对其进行GC或未将其添加到地图中。
我不确定我对这种事情的看法。您有点无法通过可迭代的WeakMap方法来修复内存管理。析构函数也可能存在第二个问题。
所有这些都会引起地狱的多个层次,因此我建议尝试通过良好的程序设计,良好实践,避免某些事情等来解决它。在JS中,这可能会令人沮丧,因为它在某些方面具有很高的灵活性,并且它更自然地是异步的,并且是基于事件的,具有大量的控制反转。
还有另一种解决方案相当优雅,但仍然存在一些潜在的严重问题。如果您具有扩展可观察类的类,则可以覆盖事件函数。仅在将事件添加到您自己时,将您的事件添加到其他可观察对象。从您删除所有事件后,再从孩子中删除事件。您也可以创建一个类来扩展您的可观察类,以为您完成此操作。这样的类可以为空值和非空值提供钩子,因此您可以观察自己。这种方法还不错,但也有麻烦。复杂度增加,性能下降。您必须保留对所观察对象的引用。至关重要的是,它也不适用于叶子,但是如果您破坏叶子,至少中间体会自毁。它' 就像链接销毁,但隐藏在您已经要链接的呼叫后面。但是,这是一个很大的性能问题,您可能需要在每次类活动时重新初始化Observable的内部数据。如果此过程花费很长时间,那么您可能会遇到麻烦。
如果您可以迭代WeakMap,则可以组合一些东西(如果没有事件,则切换为Weak,在事件时为Strong),但实际上所做的只是将性能问题放在其他人身上。
当涉及到行为时,可迭代的WeakMap也立即带来烦恼。我在前面简要提到了具有作用域引用和雕刻的函数。如果我实例化了一个子类,该子类在将侦听器'console.log(param)'挂钩到父类的构造函数中无法持久化父类,则当我删除对该子类的所有引用时,可以将其完全释放,因为将匿名函数添加到了父母没有从孩子那里引用任何东西。这留下了关于parent.weakmap.add(child,(param)=> console.log(param))的问题。据我所知,键是弱的,但不是值,因此,softmap.add(object,object)是持久的。这是我需要重新评估的东西。对我来说,如果我处理所有其他对象引用,这看起来像是内存泄漏,但实际上我怀疑它基本上是通过将其视为循环引用来进行管理的。匿名函数维护对父作用域产生的对象的隐式引用,以保持一致性,从而浪费大量内存,或者您的行为因难以预测或管理的环境而异。我认为前者实际上是不可能的。在后一种情况下,如果我在类上有一个仅接受对象并添加console.log的方法,则即使我返回了该函数并维护了一个引用,当我清除对该类的引用时,该方法也会被释放。