2012-06-25 26 views
26

Resueltomemoria riesgo de fugas en JavaScript cierres

Hay una gran cantidad de información contradictoria en la web, con respecto a este tema. Gracias a @John, me las arreglé para pensar que los cierres (como se usan a continuación) no son la causa de las pérdidas de memoria, y que, incluso en IE8, no son tan comunes como dicen las personas. De hecho, solo hubo una fuga en mi código, que no resultó tan difícil de solucionar.

A partir de ahora, mi respuesta a esta pregunta será:
yo sepa, las fugas IE8 única vez, es cuando se unen eventos/manipuladores se establecen en el objeto global. (window.onload, window.onbeforeunload, ...). Para evitar esto, vea mi respuesta a continuación.


enorme ACTUALIZACIÓN:

estoy completamente perdido ahora ... Después de cavar a través de artículos y tuts tanto antiguos como nuevos algún tiempo, me quedo con al menos un gigantesco contradicción. Mientras que uno de LA JavaScript de (Douglas Crockford) Guru dice:

Desde IE es incapaz de hacer su trabajo y recuperar los ciclos, que cae sobre nosotros para hacerlo. Si rompemos los ciclos de manera explícita, IE podrá reclamar la memoria. Según Microsoft, los cierres son la causa de pérdidas de memoria. Esto, por supuesto, está muy mal, pero lleva a Microsoft a dar muy malos consejos a los programadores sobre cómo lidiar con los errores de Microsoft. Resulta que es fácil romper los ciclos en el lado DOM. Es prácticamente imposible romperlos en el lado de JScript.

Y como @freakish señaló que mis fragmentos a continuación son similares al funcionamiento interno de jQuery, me sentí bastante seguro de que mi solución no causaba pérdidas de memoria. Al mismo tiempo encontré this MSDN page, donde la sección Circular References with Closures fue de particular interés para mí. La siguiente figura es más o menos una representación esquemática de cómo funciona mi código, ¿no es así:

Circular References with Closures

La única diferencia es que tengo el sentido común de no unir mis detectores de eventos a los propios elementos.
Todo lo mismo Douggie es bastante inequívoco: los cierres no son la fuente de mem-leaks en IE. Esta contradicción no me deja ni idea de quién tiene la razón.

También he descubierto que el problema de fuga no está completamente resuelto en IE9 tampoco (no se puede encontrar el enlace ATM).

Una última cosa: También he llegado a saber que IE maneja el DOM fuera del motor de JScript, que me pone en un punto de molestia cuando cambio los hijos de un elemento <select>, basado en una petición 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>'; 
    } 
} 

Si IE realmente se las arregla el DOM como si el motor de JScript no estuviera allí, ¿cuáles son las probabilidades de que los elementos de opción no se libera con este código?
He agregado deliberadamente este fragmento como ejemplo, porque en este caso paso variables que forman parte del ámbito de cierre como argumento para una función global.No pude encontrar ninguna documentación sobre esta práctica, pero en base a la documentación aportada por Miscrosoft, se debería romper todas las referencias circulares que podrían ocurrir, ¿no?



Advertencia: larga pregunta ... (lo siento )

He escrito un par de bastante grandes JavaScript para hacer llamadas Ajax en mi aplicación web. con el fin de evitar toneladas de devoluciones de llamada y eventos, estoy tomando el máximo provecho de delegación de eventos y cierres. Ahora he escrito una función que me hace pensar en posibles fugas de memoria. Aunque sé IE> 8 se refiere a cierres mucho mejor que sus predecesores, es política de la empresa para apoyar el IE 8 lo mismo.

A continuación he incluido un ejemplo de lo que estoy hablando de, here se puede encontrar un ejemplo similar, aunque no utiliza Ajax, pero un setTimeout, el resultado es más o menos lo mismo. (Puede, por supuesto, obviar el código de abajo, a la pregunta en sí misma)

El código que tengo en mente es la siguiente:

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

Todo material bastante sencillo, a excepción de la onreadystatechange de devolución de llamada. Me di cuenta de algunos problemas con IE cuando se enlaza directamente al controlador: xhr.onreadystatechange = callback;, por lo tanto, la función anónima. No sé por qué, pero creo que esta es la forma más fácil de hacerlo funcionar.

Como ya he dicho, estoy usando un montón de delegación de eventos, por lo que se puede imaginar que puede resultar útil para tener acceso al elemento/evento real que disparó la llamada AJAX. Así que tengo algunos controladores de eventos que se ven así:

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

Como se puede ver, el onreadystatechange devolución de llamada es el valor de retorno de una función, que proporciona la referencia al elemento target cuando la devolución de llamada. Gracias a delegación de eventos, ya no tiene que preocuparse acerca de los eventos que podrían estar ligados a ese elemento, cuando decido sacarlo de la DOM (que a veces lo hago).
En mi opinión, sin embargo, el objeto de la llamada de la función de devolución de llamada podría resultar demasiado para motor de JScript de IE y su recolector de basura:

Evento ==> == manejador> prepareAjax es una secuencia de llamada bastante normal, pero el argumento de devolución de llamada:

[anon. func (argumento t = objetivo) devuelve anon. F (t tiene acceso a lo que en las referencias volver atrás para apuntar)]
      ===> pasa a una función de devolución de llamada anon, llamada usando .apply método para el objeto XHR, a su vez, una variable privadaa la prepareAjax función

He probado esta "construcción" en FF y cromo. Funciona muy bien allí, sino que este tipo de pila de llamadas de cierre durante el cierre durante el cierre, en cada ocasión que pasa una referencia a un elemento DOM ser un problema en IE (especialmente las versiones anteriores a IE9)?


No, no voy a usar jQuery o otras librerías. Me gusta JS puro, y quiero saber todo lo que pueda sobre este lenguaje seriamente subestimado.Los fragmentos de código no son ejemplos reales de copiar y pegar, pero proporcionan, IMO, una buena representación de cómo estoy usando delegación, cierres y devoluciones de llamadas a lo largo de mi secuencia de comandos. Entonces, si alguna sintaxis no es correcta, no dude en corregirla, pero de eso no se trata esta pregunta, por supuesto.

+0

No sé qué 'xhr.onreadystatechange = callback' no funciona para usted. Interesante. Pero en realidad esta parte no debería importar en absoluto, la pérdida de memoria es lo más interesante. Nunca he visto tal comportamiento, aunque no trabajé con IE <9 por bastante tiempo. – freakish

+2

BTW: No tiene que usar jQuery, pero puede consultar su código fuente. :) Lo hice y parece que no están haciendo nada diferente a lo que eres. No debería dar pérdidas de memoria. ¿Estás seguro de que no tienes una referencia al 'xhr' en alguna parte? O al resultado 'prepareAjax'? – freakish

+0

@freakish: no espero que 'xhr.onreadystatechange = callback' sea la causa de los problemas de memoria (si hay alguno). Lo mencioné porque a mí también me resulta extraño el comportamiento de IE. Pero hace tiempo que he perdido la esperanza de que IE se comporte lógicamente. Según usted, este código puede causar problemas de memoria, eso es lo que me preocupa –

Respuesta

23

Solía ​​trabajar con el administrador de programas ex para JavaScript dentro de Microsoft, en un proyecto EcmaScript (err .. JScr ... JavaScript) que no es de navegador. Tuvimos algunas largas discusiones sobre cierres. Al final, el punto es que son más difíciles de GC, no imposibles. Tendría que leer la discusión de DC sobre cómo la MS está "equivocada" porque los cierres causan fugas de memoria, ya que en las implementaciones anteriores de IE, los cierres eran ciertamente problemáticos, porque eran muy difíciles de recolectar con la implementación de MS. Me parece extraño que un tipo de Yahoo intentara decirle a los arquitectos de MS que un problema conocido con su código estaba en otra parte. Por mucho que aprecio su trabajo, no veo qué base tenía allí.

Recuerde, el artículo que hace referencia anteriormente se refiere a IE6, ya que IE7 todavía estaba bajo un gran desarrollo en el momento de su escritura.

Como un lado, gracias a Dios, IE6 está muerto (no hagas que desentierre las imágenes del funeral). Aunque, no olvides su legado ... Todavía no he visto a nadie argumentar con credibilidad que no era el mejor navegador del mundo en el primer día de su lanzamiento. El problema fue que ganaron la guerra de los navegadores. . Y entonces, en lo que equivale a uno de los errores más grandes de su historia, despidieron al equipo y permaneció estancado durante casi 5 años. El equipo de IE se redujo a 4 o 5 muchachos que corrigieron errores durante años, creando una gran fuga de cerebros y quedando atrás de la curva dramáticamente. En el momento en que volvieron a contratar al equipo y se dieron cuenta de dónde estaban, estaban años atrás debido a la gran dificultad de lidiar con un código monolítico que nadie entendía realmente. Esa es mi perspectiva como interna en la empresa, pero no directamente vinculada a ese equipo.

Recuerde también, IE nunca optimizado para cierres porque no había ProtoypeJS (diablos, no había Rails), y jQuery era apenas un rayo en la cabeza de Resig.

En el momento de la redacción, también seguían apuntando a máquinas con 256 megas de RAM, que tampoco habían sido finalizadas.

Pensé que era justo darle esta lección de historia, después de hacerme leer todo su libro de una pregunta.

Al final, mi punto es que estás haciendo referencia al material que está muy anticuado. Sí, evite los cierres en IE6, ya que causan pérdidas de memoria, pero ¿qué no en IE6?

Al final, es un problema que MS ha abordado y sigue abordando. Vas a hacer un cierto nivel de cierres, ese fue el caso, incluso en ese momento.

Sé que hicieron un trabajo pesado en esta área alrededor de IE8 (como mi proyecto innombrable usó el motor de JavaScript estándar no actual), y ese trabajo ha continuado en IE9/10. StatCounter (http://gs.statcounter.com/) sugiere que IE7 ha reducido a una cuota de mercado del 1,5%, frente al 6% de hace un año, y en el desarrollo de sitios "nuevos", IE7 se vuelve cada vez menos relevante. También puede desarrollar NetScape 2.0, que introdujo el soporte de JavaScript, pero eso sería un poco menos tonto.

Realmente ... No intente optimizar en exceso por el bien de un motor que ya no existe.

+1

+1 por numerosas risas mientras lees tu respuesta. No tengo intención de optimizar demasiado para motores viejos. Es cierto que el enlace de DC está muy desactualizado. Pero, sinceramente, a menudo se habla de pérdidas de memoria, pero es increíblemente difícil precisar qué las está causando, por qué y dónde. El enlace de MSDN que proporcioné habla sobre referencias circulares a los objetos DOM, que se acerca mucho a lo que estoy haciendo, lo que no tiene control sobre la gestión de los elementos DOM, y me pone nervioso/nervioso. Estoy buscando una respuesta _definitiva _ a: "¿Qué causa problemas de memoria, qué navegador, por qué y cómo evitarlos?" –

+0

La respuesta corta es Flash. Cuanto más larga es la respuesta que ha golpeado en la cabeza ... no tiene acceso a la recolección de basura. La gestión de la memoria realmente depende del proveedor del navegador, y realmente será un recuadro cerrado a menos que tenga acceso (y esté dispuesto a rastrear) el código fuente, ya que los navegadores individuales en realidad no le otorgan este acceso. Las referencias circulares en el artículo de MSDN reflejan una implementación anterior de cómo IE trata con los objetos (digo IE, porque es una combinación de la secuencia de comandos DOM +), y dudaría que sea cierto hoy. –

+1

Cualquier respuesta definitiva quedará desactualizada tan pronto como los desarrolladores se den cuenta de que hay un problema y se tomarán el tiempo para solucionarlo. Además, las nuevas características agregarán nuevas fugas (Canvas). Ciertamente, hay cosas que siempre causarán pérdidas de memoria (por ejemplo, cosas persistentes en JS de alcance global o agregar un conjunto de objetos representados al DOM). Dicho esto, la pregunta es lo suficientemente vaga como para no tener una respuesta definitiva, me temo. –

4

derecha, después de pasar algún tiempo con esa herramienta IEJSLeaksDetector, me he dado cuenta de que las cosas que se habla en mi pregunta original no causan MEM FUGAS. Sin embargo, hubo una fuga que apareció.Afortunadamente, he conseguido encontrar una solución:

Tengo un script principal, y en la parte inferior, hay una vieja escuela:

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 
} 

Esto provoca una fuga en IE8, que no podía corregir con un controlador window.onbeforeunload. Parece que debes evitar unir el manejador al objeto global. La solución está en los cierres y los detectores de eventos, que es un poco faff, pero esto es lo que terminé haciendo:

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

De esa manera, el (en) evento de carga no está consolidado justo antes de las window.load regreso del guía, de acuerdo a la herramienta IEJSLeaksDetector, hay NO fugas en mi aplicación. Estoy feliz con eso. Espero que este fragmento sea útil para uno de ustedes; si alguien tiene sugerencias para mejorar este enfoque, ¡no dude en hacerlo!

¡Salud, y gracias a todos ustedes que se tomaron la molestia de leer y probar mi drible arriba!


PD: en caso de que alguien le importa, aquí está el método ucfirst Cadena:

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

¿El IEJSLeaksDetector es útil para IE 8, o solo para versiones anteriores a IE 8? – Notre

+0

Es útil para IE8, dado que se desarrolló junto con IE8 (y aún está en desarrollo activo cuando se lanzó IE8) \ –