JavaScript 闭包中的内存泄漏风险

IT技术 javascript memory-leaks cross-browser closures
2021-01-16 00:44:51

解决了

关于这个主题,网络上有很多相互矛盾的信息。感谢@John,我设法确定闭包(如下文所用)不是内存泄漏的原因,而且——即使在 IE8 中——它们并不像人们声称的那样普遍。事实上,我的代码中只发生了 1 个泄漏,这证明修复并不难。

从现在开始,我对这个问题的回答将是:
AFAIK,IE8 泄漏的唯一时间是在全局对象上附加事件/处理程序时。( window.onload, window.onbeforeunload,...)。要解决这个问题,请参阅下面我的回答。


重大更新:

我现在完全迷失了......经过一段时间的文章和新旧文章的挖掘后,我至少留下了一个巨大的矛盾。尽管之一JavaScript的大师的(道格拉斯·克罗克福德)说:

由于 IE 无法完成其工作并回收循环,因此我们必须完成它。如果我们明确打破循环,那么 IE 将能够回收内存。根据微软的说法,闭包是内存泄漏的原因。这当然是非常错误的,但它导致微软在如何处理微软的错误方面给程序员提供了非常糟糕的建议。事实证明,在 DOM 端很容易打破循环。在 JScript 端破解它们几乎是不可能的。

正如@freakish 指出的,我下面的代码片段类似于 jQuery 的内部工作方式,我对我的解决方案感到非常安全,不会导致内存泄漏。同时我发现了这个 MSDN 页面,其中的部分Circular References with Closures是我特别感兴趣的。下图几乎是我的代码如何工作的示意图,不是吗:

带闭包的循环引用

唯一的区别是我没有将我的事件侦听器附加到元素本身的常识。
尽管如此Douggie是相当明确的:闭包是不是在IE MEM-泄漏的根源。这种矛盾让我不知道谁是对的。

我还发现泄漏问题在 IE9 中也没有完全解决(找不到链接 ATM)。

最后一件事:我还了解到 IE 在 JScript 引擎之外管理 DOM,这让我在<select>基于 ajax 请求更改元素的子元素时遇到麻烦

function changeSeason(e)
{
    var xhr,sendVal,targetID;
    e = e || window.event;//(IE...
    targetID = this.id.replace(/commonSourceFragment/,'commonTargetFragment');//fooHomeSelect -> barHomeSelect
    sendVal = this.options[this.selectedIndex].innerHTML.trim().substring(0,1);
    xhr = prepareAjax(false,(function(t)
    {
        return function()
        {
            reusableCallback.apply(this,[t]);
        }
    })(document.getElementById(targetID)),'/index/ajax');
    xhr({data:{newSelect:sendVal}});
}

function reusableCallback(elem)
{
    if (this.readyState === 4 && this.status === 200)
    {
        var data = JSON.parse(this.responseText);
        elem.innerHTML = '<option>' + data.theArray.join('</option><option>') + '</option>';
    }
}

如果 IE 真的像 JScript 引擎一样管理 DOM,那么使用此代码不释放选项元素的几率有多大?
我特意添加了这个片段作为示例,因为在这种情况下,我将作为闭包作用域一部分的变量作为参数传递给全局函数。我找不到关于这种做法的任何文档,但根据 Miscrosoft 提供的文档,它应该打破任何可能发生的循环引用,不是吗?



警告:冗长的问题...(抱歉

我已经编写了几个相当大的 JavaScript 来在我的 Web 应用程序中进行 Ajax 调用。为了避免大量回调和事件,我充分利用了事件委托和关闭。现在我写了一个函数,让我想知道可能的内存泄漏。虽然我知道 IE > 8 比它的前辈更好地处理闭包,但公司政策仍然支持 IE 8。

下面我提供了一个我正在讨论的示例,在这里您可以找到一个类似的示例,虽然它不使用 ajax,但使用 setTimeout,结果几乎相同。(当然,您可以跳过下面的代码,直接看问题本身)

我想到的代码是这样的:

function prepareAjax(callback,method,url)
{
    method = method || 'POST';
    callback = callback || success;//a default CB, just logs/alerts the response
    url = url || getUrl();//makes default url /currentController/ajax
    var xhr = createXHRObject();//try{}catch etc...
    xhr.open(method,url,true);
    xhr.setRequestMethod('X-Requested-with','XMLHttpRequest');
    xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded');
    xhr.setRequestHeader('Accept','*/*');
    xhr.onreadystatechange = function()
    {
        callback.apply(xhr);
    }
    return function(data)
    {
        //do some checks on data before sending: data.hasOwnProperty('user') etc...
        xhr.send(data);
    }
}

所有非常简单的东西,除了onreadystatechange回调。在直接绑定处理程序时,我注意到 IE 的一些问题:xhr.onreadystatechange = callback;,因此匿名函数。不知道为什么,但我发现这是使其工作的最简单方法。

正如我所说,我使用了很多事件委托,因此您可以想象,访问触发 ajax 调用的实际元素/事件可能会很有用。所以我有一些看起来像这样的事件处理程序:

function handleClick(e)
{
    var target,parent,data,i;
    e = e || window.event;
    target = e.target || e.srcElement;
    if (target.tagName.toLowerCase() !== 'input' && target.className !== 'delegateMe')
    {
        return true;
    }
    parent = target;
    while(parent.tagName.toLowerCase() !== 'tr')
    {
        parent = parent.parentNode;
    }
    data = {};
    for(i=0;i<parent.cells;i++)
    {
        data[parent.cells[i].className] = parent.cells[i].innerHTML;
    }
    //data looks something like {name:'Bar',firstName:'Foo',title:'Mr.'}
    i = prepareAjax((function(t)
    {
        return function()
        {
            if (this.readyState === 4 && this.status === 200)
            {
                //check responseText and, if ok:
                t.setAttribute('disabled','disabled');
            }
        }
    })(target));
    i(data);
}

如您所见,onreadystatechange回调是函数的返回值,它target在调用回调时提供对元素的引用多亏了事件委托,当我决定从 DOM 中删除它时(我有时会这样做),我不再需要担心可能绑定到该元素的事件。
然而,在我看来,回调函数的调用对象对于 IE 的 JScript 引擎及其垃圾收集器来说可能太过分了:

Event ==> handler ==> prepareAjax 是一个非常正常的调用序列,但是回调参数:

[匿名。func(参数 t = target)返回匿名。F(可以访问 t,然后返回目标)]
   ===> 传递给匿名回调函数,使用 .apply 方法调用 xhr 对象,然后是prepareAjax 函数私有变量

我已经在 FF 和 chrome 中测试了这种“结构”。它在那里工作得很好,但是这种在关闭后关闭时的调用堆栈,每次传递对 DOM 元素的引用都会在 IE 中成为一个问题(尤其是 IE9 之前的版本)?


不,我不会使用 jQuery 或其他库。我喜欢纯 JS,并且想尽可能多地了解这种被严重低估的语言。代码片段不是实际的复制粘贴示例,但提供了 IMO,很好地表示了我如何在整个脚本中使用委托、闭包和回调。因此,如果某些语法不太正确,请随时纠正它,但这当然不是这个问题的内容。

2个回答

我曾经在一个非浏览器的 EcmaScript (err.. JScr ... JavaScript) 项目中与 Microsoft 的前 JavaScript 程序经理一起工作。我们对关闭进行了一些冗长的讨论。最后,重点是它们更难 GC,并非不可能。我必须阅读 DC 关于关闭导致内存泄漏的 MS 如何“错误”的讨论——因为在 IE 的旧实现中,关闭肯定有问题,因为它们很难用 MS 实现进行垃圾收集我觉得奇怪的是,一个雅虎人会试图告诉 MS 架构师,他们的代码的一个已知问题在其他地方。尽管我很欣赏他的工作,但我看不出他有什么依据。

请记住,您在上面引用的文章指的是 IE6,因为在撰写本文时 IE7 仍在大力开发中。

顺便说一句——谢天谢地,IE6 已经死了(不要让我挖掘葬礼图片)。尽管如此,不要忘记它的inheritance......我还没有看到有人在发布的第一天就它不是世界上最好的浏览器提出可信的论点——问题是他们赢得了浏览器战争. 因此,这相当于他们历史上最大的错误之一——他们事后解雇了这支球队,并且该球队停滞了近 5 年。多年来,IE 团队只有 4 到 5 个人在做错误修复,造成了巨大的人才流失并大大落后于曲线。当他们重新雇用团队并意识到他们所处的位置时,他们已经落后了好几年,因为处理一个没有人真正理解的单一代码库的额外工作。这是我作为公司内部人员的看法,

还要记住,IE 从来没有针对闭包进行过优化,因为没有 ProtoypeJS(见鬼,没有 Rails),而且 jQuery 在 Resig 的脑海中几乎没有一丝flash。

在撰写本文时,他们的目标仍然是具有 256 兆 RAM 的机器,这些机器也没有报废。

在让我阅读了你的整本书的问题之后,我认为给你上一节历史课是公平的。

最后,我的观点是你引用的材料已经过时了。是的,避免在 IE6 中使用闭包,因为它们会导致内存泄漏——但在 IE6 中什么没有?

归根结底,这是 MS 已经解决并将继续解决的问题。您将进行某种程度的关闭,即使在当时也是如此。

我知道他们围绕 IE8 在这方面做了大量工作(因为我的不起眼的项目使用了非当时的标准 JavaScript 引擎),并且这项工作一直持续到 IE9/10。StatCounter (http://gs.statcounter.com/) 表明 IE7 的市场份额从一年前的 6% 下降到 1.5%,并且在开发“新”站点时,IE7 变得越来越不相关。您还可以针对引入 JavaScript 支持的 NetScape 2.0 进行开发,但这只是稍微不那么愚蠢。

真的......不要为了一个不再存在的引擎而试图过度优化。

一旦开发人员意识到存在问题并花时间修复它,任何明确的答案都将过时。此外,新功能将添加新的泄漏(画布)。肯定有一些事情总是会导致内存泄漏(例如,在全局范围的 JS 中持久化事物或向 DOM 添加一堆渲染对象)。话虽如此,这个问题很模糊,恐怕没有明确的答案。
2021-03-15 00:44:51
顺便说一句,如果你错过了——我的 Flash 参考是一个如何创建内存泄漏的例子。; )
2021-03-18 00:44:51
简短的回答是 Flash。您遇到的答案越长......您无法访问垃圾收集。内存管理实际上取决于浏览器的供应商,除非您可以访问(并愿意跟踪)源代码,否则它将是一个封闭的盒子,因为个别浏览器并没有真正为您提供这种访问权限。MSDN 文章中的循环引用反映了 IE 如何处理对象的早期实现(我说 IE,因为它是 DOM+脚本的组合),我怀疑它今天是否适用。
2021-03-21 00:44:51
阅读您的答案时,会多次笑声+1。我无意对旧引擎进行过度优化。确实,直流链路已经过时了。但老实说,内存泄漏经常被谈论,但很难确定导致它们的原因、原因和位置。我提供的 MSDN 链接讨论了对 DOM 对象的循环引用,这与我正在做的非常接近,再加上无法真正控制 DOM 元素的管理,我感到好奇/紧张。我正在寻找一个 _definitive _ 答案:“是什么导致了内存问题,什么浏览器,为什么以及如何避免它们?”
2021-03-30 00:44:51
Flash:不,MSDN:它是从 2005 年开始的,很公平。但它直到 2011 年才被存档,并且它链接到了上次更新于 2007 年的知识库文章。 模糊:也许,我不太擅长解释自己。当然,不是母语人士也无济于事
2021-04-06 00:44:51

是的,在使用该IEJSLeaksDetector工具一段时间后,我发现我在原始问题中讨论的内容不会导致 MEM 泄漏但是,确实出现了 1 个泄漏。谢天谢地,我设法找到了解决方案:

我有一个主要脚本,在底部,有一个老派:

window.onload = function()
{
    //do some stuff, get URI, and call:
    this['_init' + uri[0].ucFirst()](uri);//calls func like _initController
    //ucFirst is an augmentation of the String.prototype
}

这会导致 IE8 中的泄漏,我无法使用window.onbeforeunload处理程序修复看来您必须避免将处理程序绑定到全局对象。解决方案在于闭包和事件侦听器,这有点废话,但这是我最终做的:

(function(go)
{//use closure to avoid leaking issue in IE
    function loader()
    {
        var uri = location.href.split(location.host)[1].split('/');
        //do stuff
        if (this['_init' + uri[0].ucFirst()] instanceof Function)
        {
            this['_init' + uri[0].ucFirst()](uri);
        }
        if (!(this.removeEventListener))
        {
            this.detachEvent('onload',loader);//(fix leak?
            return null;
        }
        this.removeEventListener('load',loader,false);
    }
    if (!(go.addEventListener))
    {
        go.attachEvent('onload',loader);//(IE...
    }
    else
    {
        go.addEventListener('load',loader,false);
    }
})(window);

这样window.load,根据 IEJSLeaksDetector 工具,(on)load 事件在处理程序返回之前解除绑定,我的应用程序没有泄漏。我很高兴。我希望这个片段对你们中的一个人有用 - 如果有人有改进这种方法的建议,请不要犹豫,这样做!

干杯,感谢所有经历过阅读和尝试我在上面运球的麻烦的人!


PS:如果有人关心,这里是 ucFirst String 方法:

if (!(String.prototype.ucFirst))
{
    String.prototype.ucFirst = function()
    {
        "use strict";
        return this.charAt(0).toUpperCase() + this.slice(1);
    };
}
它对 IE8 很有用,因为它是与 IE8 一起开发的(并且在 IE8 发布时仍在积极开发中)\
2021-04-02 00:44:51
IEJSLeaksDetector 是对 IE 8 有用,还是仅对 IE 8 之前的版本有用?
2021-04-08 00:44:51