Leaflet: o mapa não foi removido completamente em map.remove () do DOM

Criado em 13 set. 2017  ·  37Comentários  ·  Fonte: Leaflet/Leaflet

Como reproduzir

  • Versão do folheto que estou usando: 1.2.0
  • Navegador (com versão) que estou usando: Chrome versão 60.0.3112.113
  • Funciona bem no Firefox e Safari (não testei no IE, Edge)
  • SO / plataforma (com versão) que estou usando: macOS Sierra
  • adicionar mapa no elemento div e adicionar camada
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 );
  • Remova o mapa em alguma ação do usuário
if (this.leafletMap ){
        this.leafletMap.eachLayer(function(layer){
            layer.remove();
        });
        this.leafletMap.remove();
        this.leafletMap = null;
    }

Que comportamento estou esperando e que comportamento estou vendo

  • Após a remoção do mapa, ele remove o mapa do elemento, no entanto, se eu clicar duas vezes no div, ele gerará um erro - Uncaught TypeError: Cannot read property '_leaflet_pos' of undefined
    Parece que o elemento DOM ainda está segurando os ouvintes de evento, embora o mapa e as camadas sejam removidos.

Exemplo mínimo reproduzindo o problema

  • [] este exemplo é o mais simples possível
  • [] este exemplo não depende de nenhum código de terceiros

Usando http://playground-leaflet.rhcloud.com/ ou qualquer outro site semelhante ao jsfiddle.

needs more info

Comentários muito úteis

@spydmobile aqui vai, isso é o que eu fiz de uma forma ligeiramente modificada:
Não tenho ideia de como postar código corretamente neste campo de comentário de merda, sinto muito.
Eu editei meu próprio comentário cerca de 10 vezes agora 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 comentários

Olá e obrigado por relatar esse bug.

No entanto, parece que algo está faltando nas etapas descritas para reproduzir o problema. Eu configurei um exemplo de playground tão bem quanto poderia ao longo das etapas acima: http://playground-leaflet.rhcloud.com/rezop/edit?html , output

Neste exemplo, não consigo ver nenhum erro depois que o mapa é removido, então aparentemente há algo mais acontecendo que causa o problema que você está vendo. Você poderia fornecer mais detalhes, para que possamos rastrear isso.

@perliedman Obrigado pela resposta rápida e pelo exemplo de playground. Eu não sou capaz de reproduzir naquele. Tentando mais alguns cenários. enquanto ainda estou analisando, algumas perguntas -

Preciso remover a camada explícita, se estou fazendo map.remove() ? Meu palpite é que ele cuidará da remoção da camada também, mas você pode confirmar.

E a razão de você ter map.remove () em tempo limite é que, no exemplo, você está destruindo o mapa logo após a criação, caso contrário, não é necessário incluir um tempo limite. correto?

Lembre-se, ao trabalhar com código aberto, _use_ o código-fonte! : wink: Para responder à sua primeira pergunta, _sim_, remove removerá as camadas: https://github.com/Leaflet/Leaflet/blob/master/src/map/Map.js#L731

A razão de eu colocá-lo em um tempo limite foi para tornar meu exemplo um pouco mais realista, certificando-se de que a camada de blocos foi realmente inicializada corretamente com blocos carregados, etc. O exemplo funciona tão bem sem colocar a chamada remove um tempo limite, mas parece um pouco um teste artificial.

Olá, acho que também estou tendo esse problema. Este é meu caso de uso básico:

Estou construindo um componente do visualizador (usando o plug - in

Quando o usuário muda a visualização, estou chamando map.remove() antes de configurar um novo mapa para a nova visualização. O novo mapa é criado no mesmo elemento DOM do antigo (um div com um ID) e não estou modificando o DOM de nenhuma forma fora do Leaflet.

Na visualização inicial, tudo funciona bem. Mas depois de chamar map.remove() e mostrar uma nova visão, o console reclama: Cannot read property '_leaflet_pos' of undefined sempre que o mapa é arrastado ou ampliado.

Posso tentar postar um exemplo mínimo em algum momento, mas esse parece ser o mesmo problema. Este erro surge no Chrome, mas não no Firefox.

@egardner sim, por favor, tente criar um exemplo que reproduza isso!

@egardner exatamente o mesmo problema no aplicativo de elétron anteriormente estável (Chromium + Node) que tive que reverter para:
"folheto": "1.0.0",
"leaflet.markercluster": "1.0.0-rc.1.0"
de 1.2.0 para remover o erro Não é possível ler a propriedade '_leaflet_pos' de indefinido
Isso também aconteceu depois de map.remove () antes de recriar o mapa no mesmo elemento DOM. Não tenho tempo agora para criar um exemplo a curto prazo

Também estamos enfrentando um problema semelhante ao tentar destruir um mapa, parece que estamos mantendo referências

Vou reiterar o que disse acima: para que possamos fazer algo sobre esse problema, forneça um exemplo que reproduza o problema.

Oi. Reproduzi esse erro em um violino. em outras palavras, se você criar um mapa dentro de um elemento div, usar o método remove e preencher novamente o mapa no mesmo div, cada movimento do mapa gerará um erro
TypeError não capturado: Não é possível ler a propriedade '_leaflet_pos' de undefined.

Para reproduzir, abra meu violino, clique em remover mapa, clique em mapa de local, abra o console e mova o mapa.
http://jsfiddle.net/spydmobile/5hmadjnk/

Observe, isso só acontece em Chorme, não em FF

Sim, spydmobile, obrigado pelo exemplo, este é o mesmo erro que estou vendo no Chrome conforme relatei acima

Estou vendo o mesmo erro, mas em um caso de uso ligeiramente diferente. O mesmo erro é gerado no redimensionamento devido a uma chamada de 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)

A pilha de chamadas completa começa no manipulador _onResize . Estou usando react-leaflet mas nenhuma parte do rastreamento de pilha aponta para isso ou para o código local sendo o problema. Eu tentei algumas versões mais antigas (por exemplo, 1.0.3 e 1.2.0 ) pensando que podemos pelo menos ser capazes de bloqueá-lo em uma versão 1.x.x , mas não tive sorte.

Existe alguma atualização sobre isso? Tendo o mesmo problema aqui integrando folheto em minha aplicação. Depois que o mapa é destruído, o elemento dom ainda tem uma propriedade _leaflet_events, mas livrar-se desse objeto também não ajudou.

Parece que o contexto da função do manipulador está obsoleto (a propriedade privada _mapPane nos argumentos do manipulador aponta para um elemento inexistente).

Estou passando por isso também. Esta é uma das exceções lançadas que estou vendo:

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

Parece que isso acontece principalmente quando um usuário usa o botão Voltar do navegador para sair do mapa.

Acho que encontrei a solução:

O div do contêiner do Mapa ainda tem alguns eventos que são disparados mesmo depois de map.off e map.remove .

No meu caso, o mapa tem propriedades que começam com _leaflet_ e algumas dessas funções que encontrei no próprio mapa na propriedade "map._leaflet_events".

Esses parecem estar anexados como pointerdown , pointermove e outros, mas os nomes das propriedades são como map._leaflet_touchstarttouchstart32 etc.

Descobri que, se iterar e removê-los manualmente (usando removeEventListener seguida, anulando e excluindo a própria propriedade para uma boa medida), posso reutilizar o div para outro mapa.
Isso também pôs fim aos vazamentos de memória que eu estava observando.

Não posso postar código aqui, mas se você pesquisar a fonte do folheto por POINTER_DOWN verá os eventos anexados e saberá como destacá-los.

Posso reproduzir isso no Chrome, mas também no FF (em meu próprio projeto e com o jsfiddle fornecido por @spydmobile)

@FLoibl Talvez uma essência, violino ou algum trecho postado em outro lugar para demonstrar a técnica que você está usando com sucesso para contornar o problema?

@spydmobile aqui vai, isso é o que eu fiz de uma forma ligeiramente modificada:
Não tenho ideia de como postar código corretamente neste campo de comentário de merda, sinto muito.
Eu editei meu próprio comentário cerca de 10 vezes agora 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 backticks triplos, entendi, ty.

@FLoibl Esta é uma investigação muito boa: +1:

Você poderia adicionar algum registro ao redor ...? https://github.com/Leaflet/Leaflet/blob/5161140e952969c5da27751b79154a2c93f53bfa/src/dom/DomEvent.Pointer.js#L39 e https://github.com/Leaflet/Leaflet/blob/fe9e0f2333888e8c02b9e7f83bf337d91006ed0a/src/dom/DomEvent. js # L133

Eles deveriam estar ocorrendo para todos os eventos quando um L.Map é destruído e deveriam estar fazendo a mesma coisa que você está fazendo, mas eu me pergunto por que não funciona como esperado.

Sim, conheço essa função e vejo que é chamada, mas não para todos os eventos.

Acho que o problema é que o código os anexa como "pointermove" etc. ao dom, mas os nomes das propriedades são "touchstart" etc. Além disso, a palavra "touchstart" é vista duas vezes no nome da propriedade, talvez um inesperado doube concat do id e nome do evento?

Além disso, esses eventos de "ponteiro" devem ser anexados ao Windows 10 sem touchscreen e no Chrome?
Infelizmente, não sei o suficiente sobre o funcionamento interno dos folhetos para fornecer uma solução real :-(

Eu conheço essa função e vejo que ela é chamada, mas não para todos os eventos.

Agora a pergunta é: Quais são os eventos para os quais removePointerListener não é chamado? Talvez estejamos perdendo uma chamada de função aqui ou ali.

Além disso, esses eventos de "ponteiro" devem ser anexados ao Windows 10 sem touchscreen e no Chrome?

sim. É quase impossível detectar se um sistema possui uma tela sensível ao toque, portanto, se o navegador suportar eventos de ponteiro, presume- se que eles serão usados.

Eu não sei o suficiente sobre o funcionamento interno dos folhetos para fornecer uma solução real :-(

Ei, não se desespere, este é um ótimo trabalho de investigação! :sorriso:

Este bug não está presente na versão 1.0.3. Peguei @spydmobile jsfiddle e http://jsfiddle.net/5hmadjnk/47/ . Com a versão 1.1.0 já existe.

@ benru89 Este bug estava de fato presente em 1.0.3, na forma de https://github.com/Leaflet/Leaflet/issues/5263 (principalmente corrigido por https://github.com/Leaflet/Leaflet/pull/ 5265).

A mudança de 1.0.3 para 1.1.0 também afetou o agora obsoleto L.Mixin.Events e a compilação rollupJS, então não acho que isso possa ser rastreado muito bem, nem mesmo com git bisect .

@IvanSanchez I comparei a função remove em 1.0.3 e 1.1.0 e isto foi adicionado:

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

Se eu remover a 6ª linha, aquela que exclui o mapPane, o erro desaparece. Não sei, porém, o impacto de remover isso, acho que o mapPane tem que ser excluído, mas esse bug foi certamente introduzido quando essa linha foi adicionada.

@ benru89 Uau, essa também é uma boa informação: +1:

Só não vejo quais manipuladores de eventos de ponteiro existem nos painéis de mapa agora: pensando:

Acho que ao não remover o painel do mapa, você apenas mascara o problema. Quando rastreei a pilha de chamadas, o problema era que algum objeto _mapPane estava apontando para um elemento dom destruído, tentando assim obter uma posição em cache de indefinido. Se o painel não for destruído, os eventos fantasmas podem passar sem acionar a exceção.

@Floibl : concordo com você, acho que não é a solução, mas notei o _mapPane nulo ao verificar a pilha de chamadas, por isso tentei não remover essa linha. A solução tem que ser remover adequadamente os manipuladores de eventos, eu acho.

Acho que o manipulador de eventos que causa isso (pelo menos no meu caso e @spydmobile ) é chamado de "touchExtend", portanto, é um manipulador leaflet.draw. Descobri que remover a importação de leaflet.draw também impede as exceções.

Eu encontrei outra solução alternativa. Inicializar seu mapa com a opção não documentada touchExtend : false desativa o manipulador problemático, portanto, não há mais exceções. Eu realmente não sei quais recursos estou perdendo ao fazer isso, mas olhando para o código, podem ser alguns gestos estendidos para dispositivos móveis ou telas sensíveis ao toque ?? Em qualquer caso, no meu aplicativo tudo parece funcionar bem.

@IvanSanchez Não tenho certeza se é o mesmo problema, mas pode estar relacionado.
Ao destruir o mapa enquanto a animação de zoom está em andamento, você obtém o mesmo erro: Uncaught TypeError: Cannot read property '_leaflet_pos' of undefined .

Tentei olhar dentro do código e descobri que dentro de Map._animateZoom() há uma linha: setTimeout(Util.bind(this._onZoomTransitionEnd, this), 250);
Se bem entendi, esse tempo limite não é destruído quando o mapa é removido, então a função Map._onZoomTransitionEnd é sempre chamada. Pode ser o seu _ "falta uma chamada de função aqui ou ali" _.

E a árvore de chamadas simplificada this._onZoomTransitionEnd -> this._move -> this._getNewPixelOrigin -> this._getMapPanePos -> getPosition(this._mapPane) -> return el._leaflet_pos falha, porque this._mapPane é _undefined_.

Talvez esse caso pudesse ser corrigido, se você envolvesse as chamadas this._move e this._moveEnd na condição if (this._mapPane) {} , mas não testei se isso tem outras consequências.

Substitua este:

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

com isso:

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

Alguma atualização sobre isso? Estou tendo o mesmo problema. touchExtend : false não ajuda. O problema ocorre quando eu navego para fora da visualização, onde tenho o mapa (ele está sendo destruído neste ponto, chamando map.remove ()) e, em seguida, navego de volta para esta visualização. Ele deve criar e inicializar o novo mapa, mas estou recebendo o erro '_leaflet_pos' em getPosition no 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)

E também no 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)

Mesmo problema após map.remove (), reinicie meu mapa e obtenha este erro exato

Mesmo problema com v1.6.0. É um assunto complicado

Aqui está um SSCCE: https://jsfiddle.net/0oafw694/1/

Basicamente, executando o seguinte código…

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

… Deixa dois ouvintes de evento anexados:

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 }

Acho que zoomanim/_createAnimProxy é gerenciado por meio de unload/_destroyAnimProxy e, portanto, sem problemas. Mas moveend/_panInsideMaxBounds precisa ser cancelado. Vou preparar um PR ...

Acabei de criar um div para o mapa que tem um id dinâmico, então quando eu tenho que reutilizar o div, eu removo () o mapa existente para liberar memória (mesmo assim, alguns eventos ainda estão acontecendo) e então redesenhar o div com um id diferente para que eu crie um novo mapa nele.

Também armazeno todos os meus mapas em um objeto, para que possa manipulá-los de acordo com seu id (tenho mais de um mapa visível algumas vezes, todos com ids dinâmicos)

De acordo com minhas experiências com o folheto, quaisquer eventos (ex. Moveend, movestart, etc.) que são manipulados pelos desenvolvedores, mudam seu comportamento padrão e permanecem na memória enquanto o folheto é descarregado do dom.
Eu tinha feito isso: @moveend="()=>{enableRecenter = true}" e assim, o manipulador de 'moveend' permaneceu na memória enquanto descarregava / removia o mapa.
Eu removi as manipulações (minhas próprias implementações) desses métodos do próprio componente do mapa e agora esse erro parou de aparecer.

Então, basicamente, NÃO TOQUE NOS MÉTODOS DE MAPA NUNCA !!! Claro, a menos que a biblioteca detecte esse comportamento e corrija esse bug.

Esta página foi útil?
0 / 5 - 0 avaliações