27

解決しました

この主題に関して、ウェブ上には多くの矛盾した情報があります。@Johnのおかげで、クロージャ(以下で使用)はメモリリークの原因ではなく、IE8でも、人々が主張するほど一般的ではないことがわかりました。実際、私のコードで発生したリークは1つだけでしたが、修正するのはそれほど難しくありませんでした。

これから、この質問に対する私の答えは次のようになります
。IE8がリークするのは、イベントがアタッチされたとき、またはグローバルオブジェクトにハンドラーが設定されたときだけです。(window.onload、、window.onbeforeunload...)。これを回避するには、以下の私の答えを参照してください。


巨大な更新:

私は今完全に迷子になっています...古いものと新しいものの両方の記事やタットを掘り下げた後、私は少なくとも1つの大きな矛盾を残されています。JavaScriptの達人の1人(ダグラス・クロックフォード)は次のように述べています

IEはその仕事をしてサイクルを取り戻すことができないので、それを行うのは私たちの責任です。明示的にサイクルを中断すると、IEはメモリを再利用できるようになります。Microsoftによると、クロージャはメモリリークの原因です。もちろんこれは非常に間違っていますが、MicrosoftがMicrosoftのバグに対処する方法についてプログラマーに非常に悪いアドバイスを与えることになります。DOM側でサイクルを壊すのは簡単であることがわかります。JScript側でそれらを壊すことは事実上不可能です。

また、@ freakishが指摘したように、以下のスニペットはjQueryの内部動作に類似しているため、メモリリークを引き起こさないソリューションについてはかなり安全だと感じました。同時に、このMSDNページを見つけました。このセクションCircular References with Closuresは私にとって特に興味深いものでした。次の図は、私のコードがどのように機能するかをほぼ概略的に表したものです。

クロージャ付きの循環参照

唯一の違いは、イベントリスナーを要素自体にアタッチしないという常識があることです。
同じDouggieは非常に明確です。つまり、クロージャはIEのmem-leaksの原因ではありません。この矛盾は私に誰が正しいかについて無知なままにします。

また、IE9でもリークの問題が完全には解決されていないこともわかりました(リンクATMが見つかりません)。

最後にもう1つ<select>、IEがJScriptエンジンの外部でDOMを管理していることもわかりました。これにより、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から提供されたドキュメントに基づくと、発生する可能性のある循環参照が壊れているはずです。



警告:長い質問...(ごめんなさい

WebアプリケーションでAjax呼び出しを行うために、かなり大きなJavaScriptをいくつか作成しました。大量のコールバックとイベントを回避するために、イベントの委任とクロージャを最大限に活用しています。これで、メモリリークの可能性について疑問に思う関数を作成しました。IE> 8は、以前のバージョンよりもはるかに優れたクロージャーを処理することを私は知っていますが、IE8をすべて同じようにサポートすることは会社の方針です。

以下に、私が取り組んでいる例を示します。ここでは、同様の例を見つけることができますが、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エンジンとそのガベージコレクターにとってはあまりにも多くのことを証明するかもしれません。

イベント==>ハンドラー==>prepareAjaxはごく普通の呼び出しシーケンスですが、コールバック引数は次のとおりです。

[アノン。func(引数t = target)はanonを返します。F(tにアクセスし、ターゲットに参照します)]
   ===> xhrオブジェクトに.applyメソッドを使用して呼び出され、prepareAjax関数にプライベート変数を使用して呼び出されるanonコールバック関数に渡されます

私はこの「構造」をFFとクロムでテストしました。そこでは問題なく動作しますが、クロージャのクロージャのこの種のコールスタックは、そのたびにDOM要素への参照を渡すことがIE(特にIE9より前のバージョン)で問題になりますか?


いいえ、jQueryやその他のライブラリは使用しません。私は純粋なJSが好きで、この深刻に過小評価されている言語についてできるだけ知りたいと思っています。コードスニペットは実際のコピーアンドペーストの例ではありませんが、スクリプト全体で委任、クロージャ、コールバックをどのように使用しているかを示すIMOを提供します。したがって、構文が正しくない場合は、自由に修正してください。ただし、もちろん、それはこの質問の内容ではありません。

4

2 に答える 2

23

私は以前、Microsoft内のJavaScriptの元プログラムマネージャーと、ブラウザー以外のEcmaScript(err .. JScr ... JavaScript)プロジェクトで作業していました。私たちは閉鎖についていくつかの長い議論をしました。結局のところ、ポイントは、GCが難しく、不可能ではないということです。クロージャがメモリリークを引き起こすというMSがどのように「間違っている」かについてのDCの議論を読む必要があります-IEの古い実装では、クロージャはMS実装でガベージコレクションするのが非常に難しいため、確かに問題がありました。Yahooの人がMSアーキテクトに彼らのコードの既知の問題がどこかにあると伝えようとするのは奇妙だと思います。彼の仕事に感謝している限り、彼がそこにどのような根拠を持っていたのかわかりません。

IE7は執筆時点でまだ開発が進んでいたため、上記で参照した記事はIE6について言及していることを忘れないでください。

余談ですが、神に感謝します。IE6は死んでいます(葬儀の写真を掘り起こさないでください)。しかし、その遺産を忘れないでください...リリースの初日には世界で最高のブラウザではなかったという信頼できる議論をする人はまだいません-問題は彼らがブラウザ戦争に勝ったことでした。そして、それは彼らの歴史の大きな失敗の1つに相当します-彼らはその後チームを解雇し、それはほぼ5年間停滞していました。IEチームは、何年にもわたってバグ修正を行っていた4〜5人の人で、頭脳流出を引き起こし、劇的に遅れをとっていました。彼らがチームを再雇用し、彼らがどこにいるのかを理解するまでに、誰も本当に理解しなくなったモノロシックなコードベースを扱うという追加の雑用のために、彼らは何年も遅れていました。それが社内の私の見方です。

また、ProtoypeJSがなく(まあ、Railsもなかった)、jQueryがResigの頭の中でかろうじてちらちらしていたため、IEがクロージャー用に最適化されたことはありませんでした。

執筆時点では、彼らはまだ256メガバイトのRAMを搭載したマシンをターゲットにしていましたが、これも保守終了ではありませんでした。

質問の本全体を読ませた後、この歴史のレッスンをあなたに渡すのは公正だと思いました。

結局のところ、私のポイントは、あなたが非常に古い資料を参照しているということです。はい、メモリリークが発生するため、IE6でのクロージャは避けてください。しかし、IE6では何が起こらなかったのでしょうか。

結局、MSが対処し続けているのは問題です。当時でさえ、ある程度の閉鎖を行うつもりです。

私は彼らがIE8の周りのこの分野で重い仕事をしたことを知っています(私の言及できないプロジェクトは当時の標準ではないJavaScriptエンジンを使用していたため)、そしてその仕事はIE9/10まで続いています。StatCounter(http://gs.statcounter.com/)は、IE7の市場シェアが1年前の6%から1.5%に低下し、「新しい」サイトの開発において、IE7の関連性がますます低くなっていることを示唆しています。JavaScriptサポートを導入したNetScape2.0用に開発することもできますが、それは少し馬鹿げたことではありません。

本当に...もう存在しないエンジンのために過度に最適化しようとしないでください。

于 2012-06-26T13:52:04.140 に答える
4

そうです、そのツールでしばらく過ごした後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);

そうすれば、 IEJSLeaksDetectorwindow.loadツールによると、ハンドラーが戻る直前に(on)loadイベントがバインド解除され、アプリケーションにリークは発生しません。私はそれに満足しています。このスニペットがあなたの一人に役立つことを願っています-誰かがこのアプローチを改善するための提案を持っているなら、遠慮なくそうしてください!

乾杯、そして上記の私のドリブルを読んで試してみるのに苦労した皆さんに感謝します!


PS:誰かが気にかけている場合に備えて、これがucFirstStringメソッドです。

if (!(String.prototype.ucFirst))
{
    String.prototype.ucFirst = function()
    {
        "use strict";
        return this.charAt(0).toUpperCase() + this.slice(1);
    };
}
于 2012-07-03T10:19:39.977 に答える