来自EventSource的事件监听器会自动进行垃圾回收吗?

4

我在Stackoverflow上看到过回答提到,如果一个DOM元素超出作用域,它的事件监听器会被自动垃圾回收。那么,非DOM元素(例如EventSource)是否也是如此呢?

这里有一个示例:

function checkStatus(events) {
   return function() {
     if (events.readyState === EventSource.CLOSED) { 
       establishSSE(); // Is this fine?
     } else {
       setTimeout(checkStatus(events), 500);
     }
   }
}

function establishSSE() {
  const events = new EventSource("/sse/" + sseID);

  events.addEventListener("reply", sseReply);
  events.addEventListener("refresh", sseRefresh);

  setTimeout(checkStatus(events), 500);
}

establishSSE();

(我知道可以使用 onerror 事件。这只是一个例子)

在这种情况下,当第4行重新运行 establishSSE 时,之前的事件处理程序会被垃圾回收吗?


编辑:

我不明白这些值如何被垃圾回收。例如,以下代码可行:

function sseSubscribe() {
  let events = new EventSource("/v1/sse/mySSEID");

  events.addEventListener("message", sseHeartbeat);
  events.addEventListener("reply", sseReply);
}

也就是说,新消息仍会导致messagereply处理程序运行。

但是events在程序中再也无法访问。那么,它不应该已经被垃圾回收了吗?看起来EventSource从未被垃圾回收(除非连接断开?)


编辑#2:

看起来即使事件处理程序是匿名的,如下所示:

function sseSubscribe() {
  let events = new EventSource("/v1/sse/SSEID");

  events.addEventListener("message", function() {
    console.log("Heartbeat");
  });
 }

 sseSubscribe();

在我的端上还是可以工作的。也就是说,Heartbeat每30秒(服务器发送的间隔)会被记录在控制台中。尽管events立即超出范围并且在程序的任何其他地方都无法访问,但仍然是如此。


是的,事件源在关闭之前不会被垃圾回收 - 在此期间,它可能会从网络接收事件,并保持活动状态。 - Bergi
@Bergi 你确定关闭事件处理程序实际上允许它被垃圾回收吗?我目前正在阅读Chrome源代码,(除非我漏掉了什么),似乎调用EventSource上的close不会标记该对象可供垃圾回收。https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/eventsource/event_source.cc;l=209;bpv=0;bpt=0 - Ryan Peschel
1
我不知道在C++级别上Chrome中的GC是如何工作的,但我期望停止的计时器和取消的加载器被释放,并从全局根中删除对事件源的引用。 - Bergi
https://html.spec.whatwg.org/multipage/server-sent-events.html#garbage-collection - Kaiido
1个回答

2

问题标题的答案

是和否(来自评论)。当事件源无法在任何地方访问时,它就可以被垃圾回收1。如果它被垃圾回收了,作为内联参数声明的任何匿名事件处理程序函数addEventListener将被垃圾回收。仍然可以访问的定义更全局的函数不能被收集。

  • JavaScript值在代码中无法访问时变得适合进行垃圾回收。

  • events值的事件侦听器函数记录在对象的事件侦听器映射中:如果无法通过代码访问对象,则无法访问事件侦听器。如果唯一到达它们的方式是通过事件处理程序映射(它们是匿名的),则可以进行垃圾回收

每次调用establishSSE都会返回一个新的EventSource对象 - 作为一个对象“值”,与上次调用时返回的对象不同。

当传递给checkStatus时,events的值保存在传递给setTimeout的函数的闭包中。因此,在超时到期并执行计时器函数时,该值将被“访问”。

接下来会发生两种情况之一

  1. events中的EventSource对象仍然打开。通过调用checkStatus创建一个新的超时。这将为新的计时器回调函数创建一个新的闭包,但具有相同的events参数值。因此,(未更改的)events值仍然可以被访问,不能进行垃圾回收。

  2. events中的事件源已关闭。在这种情况下,checkStatus调用establishSSE(重新打开源),并从计时器调用返回而不在任何地方存储其参数或在可以在以后执行的闭包范围内保留它。关闭的events对象此时变得适合进行垃圾回收,因为无法访问它。

    establishSSE继续创建在名为events的变量中保存的新值,并使用它调用checkStatus,但它与调用堆栈中上面的checkStatus调用中作用域结束的值不同。

然而,由于sseReplysseRefresh没有嵌套在estabishSSE中,它们不会仅因为event对象值被回收而被回收。


1 有一种例外情况:当将 blob 或文件对象参数传递给 createObjectURL 时,它们会在内存中保留(同时网络流量检索它们的数据),必须通过调用 URL.revokeObjectURL 显式释放。


编辑回答

在问题编辑后更新

但是events现在无法从程序的任何地方访问。

这是一个无效的假设,可能导致了这个问题:

  1. checkStatusevents 参数在计时器回调函数中可访问,因为回调函数嵌套在 checkStatus 中。

  2. 回调函数被防止被垃圾回收,因为 setTimeout 的 [native code] 实现必然持有对它的引用。

  3. 如果 events 的值不处于关闭状态,则计时器回调通过使用现有的 events 对象值,通过调用 checkStatus 来生成新的回调函数,为新的 setTimeout 调用生成新的回调函数。

  4. 从第1步开始重复。

如果直接使用 checkStatus 作为计时器回调,而不是通过 setTimeout 进行延迟递归调用来传递参数,可能会更加直接和不容易混淆:

function checkStatus(events) {
    if (events.readyState === EventSource.CLOSED) { 
          establishSSE(); // create a new EventSource object
          return; // the actual argument object value is now inaccessible
                  // and becomes eligble for garbage collection.
    }
    setTimeout( checkStatus, 500, events);
}

如果实施以上更改,establishSS 还需要将 checkStatus 作为第一个参数传递给 setTimeout,但不要先调用它。
function establishSSE() {
  const events = new EventSource("/sse/" + sseID);

  events.addEventListener("reply", sseReply);
  events.addEventListener("refresh", sseRefresh);

  setTimeout(checkStatus, 500, events);
}

你确定事件处理程序映射中对它们的引用被垃圾回收了吗?因为如果是这样的话,当通过网络接收到其他数据时,处理函数肯定不会继续运行。似乎events对象离开作用域并没有垃圾回收任何东西。 - Ryan Peschel
如果事件源被关闭,该代码将使用相同的处理程序重新打开事件源。因此,如果事件继续到达,则会调用相同的函数 - 但它们位于新的事件处理程序映射中,属于新的“events”对象值,对吗? - traktor
我看到你更新了答案,只包括匿名事件处理程序被垃圾回收,但我担心这仍然是错误的,或者我可能误解了什么。我已经更新了原始帖子,解释了我的困惑。 - Ryan Peschel
当EventSource被手动关闭时,处理程序将不会被调用。这就是close函数的目的。我想说的是,events超出作用域并不会垃圾回收任何事件处理程序(即使是匿名的,这与你回答的开头相矛盾)。 - Ryan Peschel
你似乎经常改变标准。首先,你的回答说当事件超出范围时,所有事件处理程序都将被垃圾回收。然后你编辑了回答,说只有匿名事件处理程序才会被垃圾回收。现在你又再次更改你的回答,说只有在事件源手动关闭时它们才会被垃圾回收,但我认为这也可能是错误的,因为我在上面链接的Chromium源代码中,在close函数中没有任何地方表明该对象将被GC标记,所以我担心你的整个回答可能会误导人。 - Ryan Peschel
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接