Leaflet: map no se elimina por completo en map.remove () del DOM

Creado en 13 sept. 2017  ·  37Comentarios  ·  Fuente: Leaflet/Leaflet

Como reproducir

  • Versión de folleto que estoy usando: 1.2.0
  • Navegador (con la versión) que estoy usando: Chrome versión 60.0.3112.113
  • Funciona bien en Firefox y Safari (no se ha probado en IE, Edge)
  • SO / Plataforma (con versión) Estoy usando: macOS Sierra
  • agregar mapa en elemento div y agregar capa
this.leafletMap = new L.Map( <element> , {
            zoomControl: true, 
            dragging: this.isInDragMode, 
            touchZoom: false,
            scrollWheelZoom: false,
            doubleClickZoom: false,
            tap: false,
}
L.tileLayer( 'http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
                } ).addTo( this.leafletMap );
  • Eliminar el mapa en alguna acción del usuario
if (this.leafletMap ){
        this.leafletMap.eachLayer(function(layer){
            layer.remove();
        });
        this.leafletMap.remove();
        this.leafletMap = null;
    }

Qué comportamiento estoy esperando y qué comportamiento estoy viendo

  • Después de la eliminación del mapa, elimina el mapa del elemento, sin embargo, si hago doble clic en el div, arroja un error - Uncaught TypeError: Cannot read property '_leaflet_pos' of undefined
    Parece que el elemento DOM todavía contiene los oyentes de eventos a pesar de que se eliminan el mapa y las capas.

Ejemplo mínimo que reproduce el problema.

  • [] este ejemplo es lo más simple posible
  • [] este ejemplo no se basa en ningún código de terceros

Usando http://playground-leaflet.rhcloud.com/ o cualquier otro sitio similar a jsfiddle.

needs more info

Comentario más útil

@spydmobile aquí va, esto es lo que hice en una forma ligeramente modificada:
No tengo idea de cómo publicar el código correctamente en este puto campo de comentarios, lo siento por eso.
He editado mi propio comentario unas 10 veces ahora lol

function removeMap()
{
    var leafletCtrl = get_your_own_leaflet_reference_from_somewhere(), 
    dom = leafletCtrl.getReferenceToContainerDomSomehow(); 

    //This removes most of the events
    leafletCtrl.off();

//After this, the dom element should be good to reuse, unfortunatly it is not
    leafletCtrl.remove(); 

    var removeDanglingEvents = function(inputObj, checkPrefix)
    {
        if(inputObj !== null)
        {
            //Taken from the leaflet sourcecode directly, you can search for these constants and see how those events are attached, why they are never fully removed i don't know
            var msPointer = L.Browser.msPointer,
            POINTER_DOWN =   msPointer ? 'MSPointerDown'   : 'pointerdown',
            POINTER_MOVE =   msPointer ? 'MSPointerMove'   : 'pointermove',
            POINTER_UP =     msPointer ? 'MSPointerUp'     : 'pointerup',
            POINTER_CANCEL = msPointer ? 'MSPointerCancel' : 'pointercancel';

            for(var prop in inputObj)
            {
                var prefixOk = checkPrefix ? prop.indexOf('_leaflet_') !== -1 : true, propVal; //if we are in the _leaflet_events state kill everything, else only stuff that contains the string '_leaflet_'
                if(inputObj.hasOwnProperty(prop) && prefixOk)
                {
                    //Map the names of the props to the events that were really attached => touchstart equals POINTER_DOWN etc
                    var evt = []; 
                    if(prop.indexOf('touchstart') !== -1) //indexOf because the prop names are really weird 'touchstarttouchstart36' etc
                    {
                        evt = [POINTER_DOWN];
                    }
                    else if(prop.indexOf('touchmove') !== -1)
                    {
                        evt = [POINTER_MOVE];
                    }
                    else if(prop.indexOf('touchend') !== -1)
                    {
                        evt = [POINTER_UP, POINTER_CANCEL];
                    }

                    propVal = inputObj[prop];
                    if(evt.length > 0 && typeof propVal === 'function')
                    {
                        evt.each(function(domEvent)
                        {
                            dom.removeEventListener(domEvent, propVal, false);
                        });                    
                    }

                    //Reference B-GONE, Garbage b collected.
                    inputObj[prop] = null;
                    delete inputObj[prop];
                }
            }
        }        
    };

    removeDanglingEvents(dom._leaflet_events, false);
    removeDanglingEvents(dom, true);
}

Todos 37 comentarios

Hola y gracias por tomarse el tiempo para informar este error.

Sin embargo, parece que falta algo en los pasos que describe para reproducir el problema. Configuré un ejemplo de patio de recreo tan bien como pude a lo largo de los pasos anteriores: http://playground-leaflet.rhcloud.com/rezop/edit?html , output

En este ejemplo, no puedo ver ningún error después de eliminar el mapa, por lo que aparentemente hay algo más que causa el problema que está viendo. ¿Podría proporcionar más detalles para que podamos rastrear esto?

@perliedman Gracias por una respuesta rápida y por armar un ejemplo de patio de recreo. No puedo reproducir en ese. Probando algunos escenarios más. mientras todavía lo estoy investigando, un par de preguntas:

¿Necesito una eliminación explícita de capas, si estoy haciendo map.remove() ? Supongo que también se encargará de la eliminación de capas, pero ¿puede confirmarlo?

Y la razón por la que tiene map.remove () en el tiempo de espera es que en el ejemplo está destruyendo el mapa justo después de la creación, de lo contrario, no es necesario encerrarlo con un tiempo de espera. ¿correcto?

Recuerde, cuando trabaje con código abierto, ¡_utilice_ el código fuente! : wink: Para responder a su primera pregunta, _sí_, remove eliminará las capas: https://github.com/Leaflet/Leaflet/blob/master/src/map/Map.js#L731

La razón por la que lo puse en un tiempo de espera fue para hacer mi ejemplo un poco más realista, asegurándome de que la capa de mosaicos se inicializara correctamente con mosaicos cargados, etc. El ejemplo funciona igual de bien sin poner la llamada remove en un tiempo de espera, pero parece una prueba artificial.

Hola. Creo que también estoy experimentando este problema. Aquí está mi caso de uso básico:

Estoy construyendo un componente de visor (usando el complemento Leaflet-IIIF , pero no creo que eso afecte nada aquí) para objetos con múltiples páginas / superficies en lugar de mostrar un mapa real. Cuando el visor carga, hay una serie de miniaturas en las que el usuario puede hacer clic para actualizar qué vista de un objeto se muestra en el área central de la interfaz de usuario.

Cuando el usuario cambia la vista, llamo a map.remove() antes de configurar un nuevo mapa para la nueva vista. El nuevo mapa se crea en el mismo elemento DOM que el anterior (un div con una ID), y no voy a modificar el DOM de ninguna manera fuera de Leaflet.

En la vista inicial, todo funciona bien. Pero después de llamar a map.remove() y mostrar una nueva vista, la consola se queja: Cannot read property '_leaflet_pos' of undefined cada vez que se arrastra el mapa o se hace zoom.

Puedo intentar publicar un ejemplo mínimo en algún momento, pero este parece ser el mismo problema. Este error aparece en Chrome pero no en Firefox.

@egardner sí, ¡intente crear un ejemplo que reproduzca esto!

@egardner exactamente el mismo problema en la aplicación de electrones previamente estables (Chromium + Node) a la que tuve que volver:
"folleto": "1.0.0",
"leaflet.markercluster": "1.0.0-rc.1.0"
de 1.2.0 para eliminar el error No se puede leer la propiedad '_leaflet_pos' de undefined
Esto también fue después de un map.remove () antes de recrear el mapa en el mismo elemento DOM. No tengo tiempo ahora para crear y dar ejemplo a corto plazo.

También estamos experimentando un problema similar al intentar destruir un mapa, parece que se aferran a las referencias

Reiteraré lo que dije anteriormente: para que podamos hacer algo al respecto, proporcione un ejemplo que reproduzca el problema.

Hola. He reproducido este error en un violín. en pocas palabras, si crea un mapa dentro de un elemento div, luego usa el método de eliminación, luego vuelve a llenar el mapa en el mismo div, cada movimiento del mapa generará un error
Error de tipo no detectado: no se puede leer la propiedad '_leaflet_pos' de undefined.

Para reproducir, abra mi violín, haga clic en eliminar mapa, haga clic en colocar mapa, luego abra la consola y mueva el mapa.
http://jsfiddle.net/spydmobile/5hmadjnk/

Tenga en cuenta que solo sucede en Chorme, no en FF

Sí, spydmobile, gracias por el ejemplo, este es el mismo error que veo en Chrome como informé anteriormente.

Veo el mismo error pero en un caso de uso ligeramente diferente. Se produce el mismo error al cambiar el tamaño debido a una llamada invalidateSize :

Uncaught TypeError: Cannot read property '_leaflet_pos' of undefined
    at getPosition (leaflet-src.js:2765)
    at NewClass._getMapPanePos (leaflet-src.js:4378)
    at NewClass._rawPanBy (leaflet-src.js:4180)
    at NewClass.invalidateSize (leaflet-src.js:3509)
    at NewClass.<anonymous> (leaflet-src.js:4244)

La pila de llamadas completa comienza en el controlador _onResize . Estoy usando react-leaflet pero ninguna parte del seguimiento de la pila apunta a eso o al código local como el problema. Probé algunas versiones anteriores (por ejemplo, 1.0.3 y 1.2.0 ) pensando que al menos podríamos bloquearlo en una versión específica de 1.x.x , pero no tuve suerte.

¿Hay alguna actualización sobre esto? Tener el mismo problema aquí integrando el folleto en mi aplicación. Después de que se destruye el mapa, el elemento dom todavía tiene una propiedad _leaflet_events, pero deshacerse de este objeto tampoco ayudó.

Parece que el contexto de la función del controlador está obsoleto (la propiedad privada _mapPane en los argumentos del controlador apunta a un elemento inexistente).

Yo también estoy experimentando esto. Esta es una de las excepciones lanzadas que estoy viendo:

https://sentry.io/share/issue/b414c58ea85c44ee9e0e40ad0781883a/

Parece que esto sucede principalmente cuando un usuario usa el botón de retroceso del navegador para salir del mapa.

Creo que podría haber encontrado la solución:

El div contenedor Map todavía tiene algunos eventos que se activan incluso después de map.off y map.remove .

En mi caso, el mapa tiene propiedades que comienzan con _leaflet_ y algunas de esas funciones que encontré están en el mapa en la propiedad "map._leaflet_events".

Esos parecen estar adjuntos como pointerdown , pointermove y demás, pero los nombres de las Propiedades son como map._leaflet_touchstarttouchstart32 etc.

Descubrí que si los itero y los elimino manualmente (usando removeEventListener luego anulando y eliminando la propiedad en sí por si acaso), puedo reutilizar el div para otro mapa.
Esto también puso fin a las pérdidas de memoria que estaba viendo.

No puedo publicar el código aquí, pero si busca en la fuente del folleto POINTER_DOWN verá los eventos que se adjuntan y sabrá cómo separarlos.

Puedo reproducir esto en Chrome pero también en FF (tanto en mi propio proyecto como con el jsfiddle proporcionado por @spydmobile)

@FLoibl ¿ Quizás una esencia, un violín o algún fragmento publicado en otro lugar para demostrar la técnica que está utilizando con éxito para solucionar el problema?

@spydmobile aquí va, esto es lo que hice en una forma ligeramente modificada:
No tengo idea de cómo publicar el código correctamente en este puto campo de comentarios, lo siento por eso.
He editado mi propio comentario unas 10 veces ahora lol

function removeMap()
{
    var leafletCtrl = get_your_own_leaflet_reference_from_somewhere(), 
    dom = leafletCtrl.getReferenceToContainerDomSomehow(); 

    //This removes most of the events
    leafletCtrl.off();

//After this, the dom element should be good to reuse, unfortunatly it is not
    leafletCtrl.remove(); 

    var removeDanglingEvents = function(inputObj, checkPrefix)
    {
        if(inputObj !== null)
        {
            //Taken from the leaflet sourcecode directly, you can search for these constants and see how those events are attached, why they are never fully removed i don't know
            var msPointer = L.Browser.msPointer,
            POINTER_DOWN =   msPointer ? 'MSPointerDown'   : 'pointerdown',
            POINTER_MOVE =   msPointer ? 'MSPointerMove'   : 'pointermove',
            POINTER_UP =     msPointer ? 'MSPointerUp'     : 'pointerup',
            POINTER_CANCEL = msPointer ? 'MSPointerCancel' : 'pointercancel';

            for(var prop in inputObj)
            {
                var prefixOk = checkPrefix ? prop.indexOf('_leaflet_') !== -1 : true, propVal; //if we are in the _leaflet_events state kill everything, else only stuff that contains the string '_leaflet_'
                if(inputObj.hasOwnProperty(prop) && prefixOk)
                {
                    //Map the names of the props to the events that were really attached => touchstart equals POINTER_DOWN etc
                    var evt = []; 
                    if(prop.indexOf('touchstart') !== -1) //indexOf because the prop names are really weird 'touchstarttouchstart36' etc
                    {
                        evt = [POINTER_DOWN];
                    }
                    else if(prop.indexOf('touchmove') !== -1)
                    {
                        evt = [POINTER_MOVE];
                    }
                    else if(prop.indexOf('touchend') !== -1)
                    {
                        evt = [POINTER_UP, POINTER_CANCEL];
                    }

                    propVal = inputObj[prop];
                    if(evt.length > 0 && typeof propVal === 'function')
                    {
                        evt.each(function(domEvent)
                        {
                            dom.removeEventListener(domEvent, propVal, false);
                        });                    
                    }

                    //Reference B-GONE, Garbage b collected.
                    inputObj[prop] = null;
                    delete inputObj[prop];
                }
            }
        }        
    };

    removeDanglingEvents(dom._leaflet_events, false);
    removeDanglingEvents(dom, true);
}

Ah, triple backticks, lo tengo, ty.

@FLoibl Esta es una muy buena investigación: +1:

¿Podría agregar algo de registro ...? https://github.com/Leaflet/Leaflet/blob/5161140e952969c5da27751b79154a2c93f53bfa/src/dom/DomEvent . js # L133

Esos deberían estar ejecutándose para cada evento cuando se destruye un L.Map , y deberían estar haciendo lo mismo que estás haciendo, pero me pregunto por qué no funciona como se esperaba.

Sí, conozco esa función y veo que se llama, pero no para todos los eventos.

Creo que el problema es que el código los adjunta como "pointermove", etc. al dom, pero los nombres de las propiedades son "touchstart", etc. También la palabra "touchstart" se ve dos veces en el nombre de la propiedad, tal vez un doube concat inesperado del id y nombre del evento?

¿También deberían esos eventos de "puntero" incluso estar adjuntos en Windows 10 sin pantalla táctil y en Chrome?
Desafortunadamente, no sé lo suficiente sobre el funcionamiento interno de los folletos para proporcionar una solución real :-(

Conozco esa función y veo que se llama, pero no para todos los eventos.

Ahora la pregunta es: ¿Cuáles son los eventos para los que no se llama removePointerListener ? Tal vez nos falte una llamada de función aquí o allá.

¿También deberían esos eventos de "puntero" incluso estar adjuntos en Windows 10 sin pantalla táctil y en Chrome?

Si. Es casi imposible detectar si un sistema tiene una pantalla táctil, por lo que si el navegador admite eventos de puntero, se supone que se utilizarán.

No sé lo suficiente sobre el funcionamiento interno de los folletos para proporcionar una solución real :-(

¡Oye, no te desesperes, este es un gran trabajo de investigación! :sonrisa:

Este error no está presente en la versión 1.0.3. Recogí @spydmobile jsfiddle y cambié la versión del folleto y el error desaparece http://jsfiddle.net/5hmadjnk/47/ . Con la versión 1.1.0 ya está ahí.

@ benru89 Este error estaba de hecho presente en 1.0.3, en forma de https://github.com/Leaflet/Leaflet/issues/5263 (solucionado principalmente por https://github.com/Leaflet/Leaflet/pull/ 5265).

El cambio de 1.0.3 a 1.1.0 también afectó el L.Mixin.Events ahora obsoleto y la compilación rollupJS, por lo que no creo que esto se pueda rastrear bien, ni siquiera con git bisect .

@IvanSanchez comparé la función de eliminación en 1.0.3 y 1.1.0 y esto se agregó:

for (i in this._panes) {
    remove(this._panes[i]);
}
this._layers = [];
this._panes = [];
delete this._mapPane;
delete this._renderer;

Si elimino la sexta línea, la que elimina el mapPane, el error desaparece. Sin embargo, no sé el impacto de eliminar esto, supongo que el mapPane debe eliminarse, pero este error ciertamente se introdujo cuando se agregó esa línea.

@ benru89 Vaya, esa también es buena información: +1:

Sin embargo, no veo qué controladores de eventos de puntero hay en los paneles de mapa en este momento: pensando:

Creo que al no eliminar el panel del mapa, solo enmascara el problema. Cuando rastreé la pila de llamadas, el problema fue que algún objeto _mapPane apuntaba a un elemento dom destruido, tratando de obtener una posición en caché de undefined. Si el panel no se destruye, los eventos fantasma pueden pasar sin desencadenar la excepción.

@Floibl Estoy de acuerdo contigo, creo que no es la solución, pero noté el _mapPane nulo al verificar la pila de llamadas, por eso intenté no eliminar esa línea. La solución tiene que estar en el lado de eliminar correctamente los controladores de eventos, supongo.

Creo que el controlador de eventos que causa esto (al menos en mi caso y @spydmobile ) se llama "touchExtend", por lo que es un controlador leaflet.draw. Descubrí que eliminar la importación de leaflet.draw también detiene las excepciones.

Encontré otra solución. Inicializar su mapa con la opción no documentada touchExtend : false desactiva el controlador problemático, por lo que no hay más excepciones. Realmente no sé qué funciones estoy perdiendo al hacer eso, pero al mirar el código, ¿podrían ser algunos gestos extendidos para pantallas móviles o táctiles? En cualquier caso, en mi aplicación todo parece funcionar bien.

@IvanSanchez No estoy seguro de que sea el mismo problema, pero podría estar relacionado.
Cuando destruye el mapa cuando la animación de zoom está en progreso, obtiene el mismo error: Uncaught TypeError: Cannot read property '_leaflet_pos' of undefined .

Intenté mirar dentro del código y descubrí que dentro de Map._animateZoom() hay una línea: setTimeout(Util.bind(this._onZoomTransitionEnd, this), 250);
Si lo entiendo lo suficiente, este tiempo de espera no se destruye cuando se elimina el mapa, por lo que siempre se llama a la función Map._onZoomTransitionEnd . Puede ser su _ "falta una llamada de función aquí o allá" _.

Y el árbol de llamadas simplificado this._onZoomTransitionEnd -> this._move -> this._getNewPixelOrigin -> this._getMapPanePos -> getPosition(this._mapPane) -> return el._leaflet_pos falla, porque this._mapPane está _undefined_.

Quizás este caso podría arreglarse, si envuelve las llamadas this._move y this._moveEnd en la condición if (this._mapPane) {} , pero no probé si tiene otras consecuencias.

Reemplazar esto:

_onZoomTransitionEnd: function () {
    if (!this._animatingZoom) { return; }

    if (this._mapPane) {
        removeClass(this._mapPane, 'leaflet-zoom-anim');
    }

    this._animatingZoom = false;

    this._move(this._animateToCenter, this._animateToZoom);

    // This anim frame should prevent an obscure iOS webkit tile loading race condition.
    requestAnimFrame(function () {
        this._moveEnd(true);
    }, this);
}

con este:

_onZoomTransitionEnd: function () {
    if (!this._animatingZoom) { return; }

    this._animatingZoom = false;

    if (this._mapPane) {
        removeClass(this._mapPane, 'leaflet-zoom-anim');
        this._move(this._animateToCenter, this._animateToZoom);

        // This anim frame should prevent an obscure iOS webkit tile loading race condition.
        requestAnimFrame(function () {
            this._moveEnd(true);
        }, this);
    }
}

¿Alguna actualización sobre esto? Estoy experimentando el mismo problema. touchExtend : false no ayuda. El problema ocurre cuando navego fuera de la vista, donde tengo el mapa (se está destruyendo en este punto al llamar a map.remove ()) y luego navego de regreso a esta vista. Debería crear e inicializar el nuevo mapa, pero obtengo el error '_leaflet_pos' en getPosition en el método setMaxBounds :

Uncaught (in promise) TypeError: Cannot read property '_leaflet_pos' of undefined
    at getPosition (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:2445)
    at NewClass._getMapPanePos (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:4409)
    at NewClass._moved (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:4413)
    at NewClass.getCenter (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:3774)
    at NewClass.panInsideBounds (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:3488)
    at NewClass._panInsideMaxBounds (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:4220)
    at NewClass.setMaxBounds (webpack-internal:///./node_modules/leaflet/dist/leaflet-src.js:3444)

Y también en el método setView :

Uncaught (in promise) TypeError: Cannot read property '_leaflet_pos' of undefined at getPosition (leaflet-src.js?9eb7:2445) at NewClass._getMapPanePos (leaflet-src.js?9eb7:4409) at NewClass.containerPointToLayerPoint (leaflet-src.js?9eb7:3989) at NewClass._getCenterLayerPoint (leaflet-src.js?9eb7:4446) at NewClass._getCenterOffset (leaflet-src.js?9eb7:4451) at NewClass._tryAnimatedPan (leaflet-src.js?9eb7:4526) at NewClass.setView (leaflet-src.js?9eb7:3181)

El mismo problema después de map.remove (), reinicie mi mapa y obtenga este error exacto

Mismo problema con v1.6.0. Es un tema complicado

Aquí hay un SSCCE: https://jsfiddle.net/0oafw694/1/

Básicamente, ejecutando el siguiente código ...

map = L.map('map');
map.setView(...);
map.setMaxBounds(...);
map.remove();

... deja dos oyentes de eventos adjuntos:

moveend: (1) […]
0: Object { fn: _panInsideMaxBounds(), ctx: undefined } // from setMaxBounds

unload: (2) […]
0: Object { fn: _destroy() , ctx: {…} }
1: Object { fn: _destroyAnimProxy(), ctx: undefined }

zoomanim: (1) […]
0: Object { fn: _createAnimProxy(), ctx: undefined }

Supongo que zoomanim/_createAnimProxy se maneja a través de unload/_destroyAnimProxy , y por lo tanto no hay problema. Pero es necesario anular el registro de moveend/_panInsideMaxBounds . Prepararé un PR ...

Acabo de terminar creando un div para el mapa que tiene una identificación dinámica, por lo que cuando tengo que reutilizar el div, elimino () el mapa existente para liberar memoria (aun así, hay algunos eventos que siguen circulando) y luego vuelva a dibujar el div con una identificación diferente, así que creo un nuevo mapa en él.

También almaceno todos mis mapas en un objeto, por lo que puedo manipularlos de acuerdo con su identificación (tengo más de un mapa visible algunas veces, todos con identificadores dinámicos)

Según mis experimentos con el folleto, cualquier evento (por ejemplo, moveend, movestart, etc.) que sean manipulados por los desarrolladores, cambia su comportamiento predeterminado y permanece en la memoria mientras se descarga el folleto del dom.
Hice esto: @moveend="()=>{enableRecenter = true}" y así, el controlador de 'moveend' permaneció en la memoria mientras descargaba / eliminaba el mapa.
Eliminé las manipulaciones (mis propias implementaciones) de estos métodos del componente del mapa y ahora este error dejó de aparecer.

Básicamente, ¡¡¡NO TOQUES NUNCA LOS MÉTODOS DEL MAPA !!! Por supuesto, a menos que la biblioteca detecte este comportamiento y corrija este error.

¿Fue útil esta página
0 / 5 - 0 calificaciones