2012-06-25 17 views
26

risoltoMemoria rischio di perdite in JavaScript chiusure

C'è un sacco di informazioni contraddittorie sul web, per quanto riguarda questo argomento. Grazie a @John, sono riuscito a capire che le chiusure (come usate di seguito) non sono la causa delle perdite di memoria, e che, anche in IE8, non sono così comuni come affermano le persone. In effetti c'era solo una perdita che si è verificata nel mio codice, il che non è stato così difficile da risolvere.

Da ora in poi, la mia risposta a questa domanda sarà:
per quanto ne so, l'unica volta che le perdite IE8, è quando sono attaccati gli eventi/gestori sono impostati sul oggetto globale. (window.onload, window.onbeforeunload, ...). Per aggirare questo, vedi la mia risposta qui sotto.


ENORME UPDATE:

sto completamente perso ora ... Dopo un po 'di tempo a scavare attraverso articoli e tuts vecchi e nuovi, io sono rimasto con almeno un humongous contraddizione. Mentre uno dei di LA JavaScript Guru (Douglas Crockford) dice:

Dal IE è in grado di fare il suo lavoro e recuperare i cicli, si cade su di noi per farlo. Se interrompiamo esplicitamente i cicli, allora IE sarà in grado di recuperare la memoria. Secondo Microsoft, le chiusure sono la causa delle perdite di memoria. Questo è ovviamente profondamente sbagliato, ma porta a Microsoft dare consigli molto male ai programmatori su come affrontare i bug di Microsoft. Risulta che è facile rompere i cicli sul lato DOM. È praticamente impossibile romperli sul lato JScript.

E come @freakish ha sottolineato che i miei frammenti di seguito sono simili ai meccanismi interni di jQuery, mi sentivo piuttosto sicuro che la mia soluzione non causasse perdite di memoria. Allo stesso tempo ho trovato this MSDN page, dove la sezione Circular References with Closures era di particolare interesse per me. La figura qui sotto è praticamente una rappresentazione schematica di come funziona il mio codice, non è vero:

Circular References with Closures

L'unica differenza è che ho il buon senso di non attaccare i miei ascoltatori di eventi per gli elementi stessi.
Tutti uguali Douggie è abbastanza inequivocabile: chiusure non sono la fonte di perdite di mem in IE. Questa contraddizione mi lascia senza tracce su chi ha ragione.

Ho anche scoperto che il problema di perdita non è completamente risolto in IE9 (non è possibile trovare il collegamento ATM).

Un'ultima cosa: ho anche venuto a sapere che IE gestisce il DOM all'esterno del motore JScript, che mi mette in un po 'di fastidio quando cambio i figli di un elemento <select>, sulla base di una richiesta 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>'; 
    } 
} 

Se IE realmente riesce il DOM come se il motore JScript non ci fosse, quali sono le probabilità che gli elementi di opzione non vengono deallocate utilizzando questo codice?
Ho deliberatamente aggiunto questo snippet come esempio, perché in questo caso sto passando variabili che fanno parte dell'ambito di chiusura come argomento di una funzione globale.Non sono riuscito a trovare alcuna documentazione su questa pratica, ma sulla base della documentazione fornita da Miscrosoft, dovrebbe interrompere qualsiasi riferimento circolare che potrebbe verificarsi, non è vero?



Attenzione: lunga domanda ... (scusate )

Ho scritto un paio di abbastanza grandi JavaScript per effettuare chiamate Ajax nella mia applicazione web. Per evitare tonnellate di callback ed eventi, sto sfruttando appieno la delega e la chiusura degli eventi. Ora ho scritto una funzione che mi chiede quali possibili perdite di memoria. Anche se so che IE> 8 riguarda le chiusure molto meglio dei suoi predecessori, è la politica aziendale a supportare lo IE 8 allo stesso modo.

Di seguito ho fornito un esempio di quello di cui sto parlando, here è possibile trovare un esempio simile, sebbene non utilizzi ajax, ma un setTimeout, il risultato è praticamente lo stesso. (È possibile, naturalmente saltare il codice qui sotto, alla domanda stessa)

Il codice che ho in mente è questa:

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); 
    } 
} 

Tutti roba abbastanza straight-forward, fatta eccezione per la onreadystatechange callback. Ho notato alcuni problemi con IE durante l'associazione diretta del gestore: xhr.onreadystatechange = callback;, quindi la funzione anonima. Non so perché, ma ho trovato questo il modo più semplice per farlo funzionare.

Come ho detto, sto usando un sacco di deleghe di eventi, quindi puoi immaginare che possa rivelarsi utile avere accesso all'elemento/evento reale che ha generato la chiamata ajax. Così ho alcuni gestori di eventi che assomigliano a questo:

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); 
} 

Come si può vedere, la richiamata onreadystatechange è il valore di ritorno di una funzione, che fornisce il riferimento all'elemento target quando il callback viene chiamata. Grazie alla delega degli eventi, non devo più preoccuparmi degli eventi che potrebbero essere associati a quell'elemento, quando decido di rimuoverlo dal DOM (cosa che a volte faccio).
A mio parere, tuttavia, l'oggetto richiamo della funzione di callback potrebbe rivelarsi troppo per il motore JScript di IE e la sua garbage collector:

evento ==> handler ==> prepareAjax è un abbastanza normale sequenza di chiamata, ma l'argomento del callback:

[anon. func (argomento t = target) restituisce anon. F (t ha accesso a cui nei rif tornare indietro a bersaglio)]
      ===> passato a una funzione anon richiamata, chiamata utilizzando .Applicare metodo per l'oggetto XHR, a sua volta privato variabile al prepareAjax funzione

ho testato questa "costruzione" in FF e Chrome. Funziona benissimo lì, ma questo tipo di callstack di chiusura alla chiusura alla chiusura, ogni volta che passa un riferimento ad un elemento DOM è un problema in IE (specialmente versioni precedenti a IE9)?


No, non ho intenzione di utilizzare jQuery o altre librerie. Mi piace il puro JS e voglio sapere il più possibile su questo linguaggio gravemente sottovalutato.I frammenti di codice non sono veri esempi di copia-incolla, ma forniscono, IMO, una buona rappresentazione di come sto usando la delega, chiusure e callback in tutto il mio script. Quindi se qualche sintassi non è corretta, sentiti libero di correggerla, ma non è di questo che si tratta, naturalmente.

+0

Non so il motivo per cui 'xhr.onreadystatechange = callback' non funziona per voi. Interessante. Ma in realtà questa parte non dovrebbe importare affatto, la perdita di memoria è la cosa più interessante. Non ho mai visto un comportamento del genere, anche se non ho lavorato con IE <9 per un po 'di tempo. – freakish

+2

BTW: Non devi usare jQuery, ma puoi guardare il suo codice sorgente. :) L'ho fatto e sembra che non stiano facendo nulla di diverso allora. Non dovrebbe dare perdite di memoria. Sei sicuro di non avere un riferimento al 'xhr' da qualche parte? O per il risultato 'prepareAjax'? – freakish

+0

@freakish: non mi aspetto che 'xhr.onreadystatechange = callback' sia la causa dei problemi di memoria (se ce ne sono). L'ho menzionato perché anch'io trovo strano comportamento da parte di IE. Ma ho perso da tempo la speranza che IE si comporti logicamente. Secondo te, questo codice potrebbe causare problemi di memoria, questo è quello che mi preoccupa per –

risposta

23

Ero abituato a lavorare con il gestore di programmi esterni per JavaScript all'interno di Microsoft, in un progetto EcmaScript (err .. JScr ... JavaScript) non di browser. Abbiamo avuto lunghe discussioni sulle chiusure. Alla fine, il punto è che sono più difficili da GC, non impossibile. Dovrei leggere la discussione di DC su come la SM sia "sbagliata", poiché le chiusure causano perdite di memoria, poiché nelle vecchie implementazioni di IE le chiusure erano certamente problematiche, poiché erano molto difficili da raccogliere con l'implementazione MS. Trovo strano che un ragazzo di Yahoo provi a dire agli architetti MS che un problema noto con il loro codice era da qualche altra parte. Per quanto apprezzi il suo lavoro, non vedo che base avesse lì.

Ricorda, l'articolo che hai fatto riferimento in precedenza si riferisce a IE6, poiché IE7 era ancora in fase di forte sviluppo al momento della sua stesura.

Come parte - grazie a dio, IE6 è morto (non farmi scovare le foto funebri). Anche se, non dimenticate la sua eredità ... Devo ancora vedere qualcuno fare un argomento credibile che non era il miglior browser al mondo il giorno 1 della sua uscita - il problema era che hanno vinto la guerra dei browser . E così, in quello che è uno dei più grandi errori della loro storia, hanno licenziato la squadra in seguito e sono rimasti stagnanti per quasi 5 anni. Il team di IE è stato ridotto a 4 o 5 ragazzi facendo correzioni di bug per anni, creando un'enorme fuga di cervelli e cadendo drasticamente dietro la curva. Nel momento in cui hanno riassunto la squadra e capito dove erano, sono stati indietro di anni a causa dell'aggiunta aggiunta di occuparsi di una base di codici monolingue che nessuno ha più capito. Questa è la mia prospettiva come interna all'azienda, ma non direttamente legata a quella squadra.

Ricordate anche che IE non è mai stato ottimizzato per chiusure perché non esisteva ProtoypeJS (diamine, non c'erano Rails) e jQuery era a malapena un barlume nella testa di Resig.

Al momento della stesura, stavano anche puntando ancora a macchine con 256 mega di RAM, che non erano nemmeno state messe fine alla vita.

Ho pensato che fosse giusto consegnarti questa lezione di storia, dopo avermi fatto leggere l'intero libro di una domanda.

Alla fine, il mio punto è che si sta facendo riferimento a materiale che è estremamente datato. Sì, evita chiusure in IE6, in quanto causano perdite di memoria - ma cosa non ha fatto in IE6?

Alla fine, è un problema che MS ha affrontato e continua a risolvere. Farai un certo livello di chiusure, così è stato, anche allora.

So che hanno svolto un lavoro pesante in questo settore intorno a IE8 (poiché il mio progetto non menzionabile utilizzava il motore JavaScript standard non standard) e che il lavoro è proseguito in IE9/10. StatCounter (http://gs.statcounter.com/) suggerisce che IE7 scende a una quota di mercato dell'1,5%, in calo rispetto al 6% di un anno fa, e nello sviluppo di "nuovi" siti, IE7 diventa sempre meno rilevante. È inoltre possibile sviluppare per NetScape 2.0, che ha introdotto il supporto JavaScript, ma sarebbe solo leggermente meno sciocco.

Davvero ... Non cercare di ottimizzare eccessivamente per un motore che non esiste più.

+1

+1 per numerose risate mentre leggi la tua risposta. Non ho intenzione di sovra-ottimizzare per i vecchi motori. È vero, il collegamento DC è obsoleto. Ma onestamente, si parla spesso di perdite di memoria, ma è incredibilmente difficile individuare cosa le causa, perché e dove. Il link MSDN che ho fornito parla di riferimenti circolari agli oggetti DOM, che si avvicina molto a quello che sto facendo, accoppiandolo per non avere un controllo reale sulla gestione degli elementi DOM, e divento curioso/nervoso. Sto cercando una risposta _definitiva a: "Che cosa causa problemi con mem, quale browser, perché e come evitarli?" –

+0

La risposta breve è Flash. La risposta più lunga che hai colpito in testa ... non hai accesso alla garbage collection. La gestione della memoria dipende molto dal venditore del browser e sarà davvero una casella chiusa a meno che tu non abbia accesso (e sia disposto a rintracciare) il codice sorgente, dal momento che i singoli browser non ti danno realmente questo accesso. I riferimenti circolari nell'articolo MSDN riflettono una precedente implementazione di come IE si occupa di oggetti (dico IE, perché è una combinazione dello script DOM +), e dubito che sia vero oggi. –

+1

Qualsiasi risposta definitiva sarà superata non appena gli sviluppatori si renderanno conto che c'è un problema e prendersi il tempo necessario per risolverlo. Inoltre, nuove funzionalità aggiungeranno nuove perdite (Canvas). Ci sono certamente cose che causeranno sempre perdite di memoria (per esempio, persistendo cose nello scope globale JS o aggiungendo un mucchio di oggetti renderizzati al DOM). Detto questo, la domanda è abbastanza vaga da non avere una risposta definitiva, temo. –

4

Destra, dopo aver trascorso qualche tempo con quella IEJSLeaksDetector strumento, ho scoperto che le cose che ho parlato nella mia domanda iniziale non causano perdite MEM. Tuttavia, c'è stata una perdita che si è verificata.Per fortuna, sono riuscito a trovare una soluzione:

Ho uno script principale, e in fondo, c'è una vecchia scuola:

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 
} 

Ciò causa una perdita in IE8, che non potevo risolvere con un gestore window.onbeforeunload. Sembra che devi evitare di legare il gestore all'oggetto globale. La soluzione sta nel chiusure e listener di eventi, è un po 'un faff, ma ecco quello che ho finito per fare:

(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); 

In questo modo, il (a) evento carico è non legato poco prima delle window.load conduttore torni, secondo allo strumento IEJSLeaksDetector, ci sono NO perdite nella mia applicazione. Sono felice con quello. Spero che questo frammento sia di qualche utilità per qualcuno di voi - se qualcuno ha suggerimenti per migliorare questo approccio, non esitate a farlo!

Saluti, e grazie a tutti voi che avete passato la briga di leggere e provare il mio dribbling sopra!


PS: nel caso in cui qualcuno si preoccupa, ecco il metodo ucfirst String:

if (!(String.prototype.ucFirst)) 
{ 
    String.prototype.ucFirst = function() 
    { 
     "use strict"; 
     return this.charAt(0).toUpperCase() + this.slice(1); 
    }; 
} 
+0

IEJSLeaksDetector è utile per IE 8 o solo per versioni precedenti a IE 8? – Notre

+0

È utile per IE8, dato che è stato sviluppato lungo IE8 (e ancora in sviluppo attivo quando è stato rilasciato IE8) \ –