Leaflet: карта не удаляется полностью на map.remove () из DOM

Созданный на 13 сент. 2017  ·  37Комментарии  ·  Источник: Leaflet/Leaflet

Как воспроизвести

  • Версия листовки, которую я использую: 1.2.0
  • Браузер (с версией) Я использую: Версия Chrome 60.0.3112.113
  • Он отлично работает в Firefox и Safari (не тестировался в IE, Edge)
  • ОС / платформа (с версией) Я использую: macOS Sierra
  • добавить карту в элемент div и добавить слой
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 );
  • Удалить карту при каком-либо действии пользователя
if (this.leafletMap ){
        this.leafletMap.eachLayer(function(layer){
            layer.remove();
        });
        this.leafletMap.remove();
        this.leafletMap = null;
    }

Какого поведения я ожидаю и какое поведение наблюдаю

  • После удаления карты она удаляет карту из элемента, однако, если я дважды щелкаю по div, выдается ошибка - Uncaught TypeError: Cannot read property '_leaflet_pos' of undefined
    Похоже, что элемент DOM все еще удерживает слушателей событий, хотя карта и слои удалены.

Минимальный пример воспроизведения проблемы

  • [] этот пример максимально простой
  • [] этот пример не полагается на какой-либо сторонний код

Используя http://playground-leaflet.rhcloud.com/ или любой другой сайт, похожий на jsfiddle.

needs more info

Самый полезный комментарий

Вот и @spydmobile, вот что я сделал в слегка измененной форме:
Я понятия не имею, как правильно разместить код в этом гребаном поле комментария, извините за это.
Я уже 10 раз редактировал свой комментарий, лол

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

Все 37 Комментарий

Привет и спасибо, что нашли время сообщить об этой ошибке.

Однако похоже, что в описанных вами шагах чего-то не хватает, чтобы воспроизвести проблему. Я создал пример игровой площадки, как и в описанных выше шагах: http://playground-leaflet.rhcloud.com/rezop/edit?html , output

В этом примере я не вижу никаких ошибок после удаления карты, поэтому очевидно, что происходит что-то еще, что вызывает проблему, которую вы видите. Не могли бы вы предоставить более подробную информацию, чтобы мы могли это отследить.

@perliedman Спасибо за быстрый ответ и за создание примера игровой площадки. Я не могу воспроизвести на этом. Пробуем еще несколько сценариев. пока я все еще изучаю это, пара вопросов -

Мне нужно явное удаление слоя, если я делаю map.remove() ? Я предполагаю, что он также позаботится об удалении слоя, но вы можете подтвердить.

Причина, по которой у вас есть map.remove () при тайм-ауте, заключается в том, что в этом примере вы уничтожаете карту сразу после создания, иначе это не требуется заключать в тайм-аут. верный?

Помните, что при работе с открытым исходным кодом _используйте_ исходный код! : wink: Чтобы ответить на ваш первый вопрос, _yes_, remove удалит слои: https://github.com/Leaflet/Leaflet/blob/master/src/map/Map.js#L731

Причина, по которой я поместил его в тайм-аут, заключалась в том, чтобы сделать мой пример немного более реалистичным, убедившись, что слой плитки действительно правильно инициализирован с загруженными плитками и т. Д. Пример работает так же хорошо, не помещая вызов remove в тайм-аут, но это похоже на искусственный тест.

Привет! Думаю, я тоже столкнулся с этой проблемой. Вот мой основной вариант использования:

Я создаю компонент просмотра (используя плагин Leaflet-IIIF , но я не думаю, что это на что-то влияет) для объектов с несколькими страницами / поверхностями, а не для отображения реальной карты. Когда средство просмотра загружается, появляется серия эскизов, которые пользователь может щелкнуть, чтобы обновить, какой вид объекта отображается в центральной области пользовательского интерфейса.

Когда пользователь меняет представление, я вызываю map.remove() перед настройкой новой карты для нового представления. Новая карта создается на том же элементе DOM, что и старая (div с идентификатором), и я никоим образом не изменяю DOM за пределами Leaflet.

На первый взгляд все работает нормально. Но после вызова map.remove() и отображения нового представления консоль жалуется: Cannot read property '_leaflet_pos' of undefined всякий раз, когда карта перетаскивается или масштабируется.

В какой-то момент я могу попытаться опубликовать минимальный пример, но, похоже, это та же проблема. Эта ошибка возникает в Chrome, но не в Firefox.

@egardner: да, попробуйте создать пример, воспроизводящий это!

@egardner точно
"листовка": "1.0.0",
"leaflet.markercluster": "1.0.0-rc.1.0"
из 1.2.0 для удаления ошибки Невозможно прочитать свойство _leaflet_pos из undefined
Это также было после map.remove () перед воссозданием карты в том же элементе DOM. У меня сейчас нет времени создавать и примерять в краткосрочной перспективе

Мы также сталкиваемся с аналогичной проблемой, пытаясь уничтожить карту, похоже, она цепляется за ссылки.

Я повторю то, что я сказал выше: чтобы мы могли что-то сделать с этой проблемой, предоставьте пример, воспроизводящий проблему.

Привет. Я воспроизвел эту ошибку на скрипке. Проще говоря, если вы создаете карту внутри элемента div, затем используете метод remove, а затем повторно заполняете карту в том же div, каждое перемещение карты будет генерировать ошибку
Uncaught TypeError: невозможно прочитать свойство _leaflet_pos, равное undefined.

Чтобы воспроизвести, откройте мою скрипку, щелкните удалить карту, щелкните карту места, затем откройте консоль и переместите карту.
http://jsfiddle.net/spydmobile/5hmadjnk/

Обратите внимание, это происходит только в Chorme, а не в FF.

Да, spydmobile, спасибо за пример, это та же ошибка, которую я вижу в Chrome, как я сообщал выше

Я вижу ту же ошибку, но в другом варианте использования. Та же ошибка возникает при изменении размера из-за вызова 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)

Полный стек вызовов начинается с обработчика _onResize . Я использую react-leaflet но никакая часть трассировки стека не указывает на это или на локальный код, являющийся проблемой. Я попробовал несколько более старых версий (например, 1.0.3 и 1.2.0 ), думая, что мы, по крайней мере, сможем привязать его к определенной версии 1.x.x , но мне не повезло.

Есть какие-нибудь обновления по этому поводу? Имея ту же проблему, интегрируя буклет в мое приложение. После уничтожения карты элемент dom все еще имеет свойство _leaflet_events, но избавление от этого объекта тоже не помогло.

Кажется, что контекст функции-обработчика устарел (частное свойство _mapPane в аргументах обработчика указывает на несуществующий элемент).

Я тоже это переживаю. Это одно из возникающих исключений, которые я вижу:

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

Похоже, что это чаще всего происходит, когда пользователь использует кнопку браузера «Назад», чтобы покинуть карту.

Думаю, я нашел решение:

В div контейнера Map все еще есть некоторые события, которые запускаются даже после map.off и map.remove .

В моем случае карта имеет свойства, которые начинаются с _leaflet_ и некоторые из этих функций, которые я обнаружил на самой карте в свойстве "map._leaflet_events".

Кажется, что они прикреплены как pointerdown , pointermove и т. Д., Но имена свойств похожи на map._leaflet_touchstarttouchstart32 и т. Д.

Я обнаружил, что если я повторяю их и удаляю вручную (используя removeEventListener затем обнуляя и удаляя само свойство для хорошей меры), я могу повторно использовать div для другой карты.
Это также положило конец наблюдаемым мною утечкам памяти.

Я не могу разместить здесь код, но если вы выполните поиск в источнике листовок по запросу POINTER_DOWN вы увидите прикрепляемые события и узнаете, как их отсоединить.

Я могу воспроизвести это в Chrome, но также и в FF (как в моем собственном проекте, так и с помощью jsfiddle, предоставленного @spydmobile)

@FLoibl Может быть, суть, скрипка или какой-то фрагмент, опубликованный в другом месте, демонстрирует технику, которую вы успешно используете для обходного пути?

Вот и @spydmobile, вот что я сделал в слегка измененной форме:
Я понятия не имею, как правильно разместить код в этом гребаном поле комментария, извините за это.
Я уже 10 раз редактировал свой комментарий, лол

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

А, тройные обратные кавычки, понял, ты.

@FLoibl Это очень хорошее расследование: +1:

Не могли бы вы добавить логирование ...? https://github.com/Leaflet/Leaflet/blob/5161140e952969c5da27751b79154a2c93f53bfa/src/dom/DomEvent.Pointer.js#L39 и https://github.com/Leaflet/Leaflet/blob/fe9e0f2333888e8c02b9e7f83bf337d91006ed0a/src/dom/DomEvent. js # L133

Они должны запускаться для каждого события, когда L.Map уничтожается, и должны делать то же самое, что и вы, но мне интересно, почему это не работает так, как ожидалось.

Да, я знаю эту функцию и вижу, что она вызывается, но не для всех событий.

Я думаю, проблема в том, что код прикрепляет их как «pointermove» и т. Д. К dom, но имена свойств - «touchstart» и т. Д. Также слово «touchstart» встречается дважды в имени свойства, возможно, неожиданное двойное соединение id и название события?

Также должны ли эти события «указателя» быть прикреплены к Windows 10 без сенсорного экрана и в Chrome?
К сожалению, я недостаточно знаю о внутренней работе листовок, чтобы обеспечить реальное исправление :-(

Я знаю эту функцию и вижу, что она вызывается, но не для всех событий.

Теперь вопрос: для каких событий removePointerListener не вызывается? Возможно, нам здесь или там не хватает вызова функции.

Также должны ли эти события «указателя» быть прикреплены к Windows 10 без сенсорного экрана и в Chrome?

да. Практически невозможно определить, есть ли в системе сенсорный экран, поэтому, если браузер поддерживает события указателя, предполагается , что они будут использоваться.

я не знаю достаточно о внутренней работе листовок, чтобы обеспечить реальное исправление :-(

Эй, не отчаивайтесь, это отличное расследование! :улыбка:

Этой ошибки нет в версии 1.0.3. Я взял @spydmobile jsfiddle и изменил версию листовки, и ошибка исчезла http://jsfiddle.net/5hmadjnk/47/ . В версии 1.1.0 он уже есть.

@ benru89 Фактически эта ошибка присутствовала в 1.0.3 в виде https://github.com/Leaflet/Leaflet/issues/5263 (в основном исправлена ​​https://github.com/Leaflet/Leaflet/pull/ 5265).

Изменение с 1.0.3 на 1.1.0 также повлияло на устаревшую L.Mixin.Events и сборку rollupJS, поэтому я не думаю, что это можно хорошо отследить, даже с помощью git bisect .

@IvanSanchez Я сравнил функцию удаления в 1.0.3 и 1.1.0 и добавил:

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

Если я удалю 6-ю строку, ту, которая удаляет mapPane, ошибка исчезнет. Я не знаю, однако, влияние удаления этого, я предполагаю, что mapPane должен быть удален, но эта ошибка определенно появилась, когда эта строка была добавлена.

@ benru89 Вау, это тоже хорошая информация: +1:

Я просто не вижу, какие обработчики событий указателя сейчас есть на панелях карты: Think:

Я думаю, что, не удаляя панель карты, вы только замаскируете проблему. Когда я отслеживал стек вызовов, проблема заключалась в том, что какой-то объект _mapPane указывал на разрушенный элемент dom, таким образом пытаясь получить кешированную позицию из undefined. Если панель не уничтожена, призрачные события могут пройти, не вызывая исключения.

@Floibl Я согласен с вами, я думаю, что это не решение, но я заметил нулевой _mapPane при проверке стека вызовов, поэтому я попытался не удалять эту строку. Решение должно быть на стороне правильного удаления обработчиков событий, я думаю.

Я думаю, что вызывающий это обработчик событий (по крайней мере, в моем случае и в @spydmobile ) называется touchExtend, так что это обработчик leaflet.draw. Я обнаружил, что удаление импорта для leaflet.draw также останавливает исключения.

Я нашел другое решение. Инициализация карты с недокументированной опцией touchExtend : false деактивирует проблемный обработчик, поэтому больше никаких исключений. Я действительно не знаю, какие функции я теряю, делая это, но, глядя на код, это могут быть какие-то расширенные жесты для мобильных или сенсорных экранов? В любом случае в моем приложении все работает нормально.

@IvanSanchez Я не уверен, что это та же проблема, но может быть связана.
Когда вы уничтожаете карту во время анимации масштабирования, вы получаете ту же ошибку: Uncaught TypeError: Cannot read property '_leaflet_pos' of undefined .

Я попытался заглянуть внутрь кода и обнаружил, что внутри Map._animateZoom() есть строчка: setTimeout(Util.bind(this._onZoomTransitionEnd, this), 250);
Насколько я понимаю, этот тайм-аут не уничтожается при удалении карты, поэтому всегда вызывается функция Map._onZoomTransitionEnd . Это может быть ваш _ "пропущенный вызов функции здесь или там" _.

И упрощенное дерево вызовов this._onZoomTransitionEnd -> this._move -> this._getNewPixelOrigin -> this._getMapPanePos -> getPosition(this._mapPane) -> return el._leaflet_pos не работает, потому что this._mapPane _неопределено_.

Возможно, этот случай можно исправить, если вы включите вызовы this._move и this._moveEnd в условие if (this._mapPane) {} , но я не проверял, имеет ли это какие-то другие последствия.

Замените это:

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

с этим:

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

Есть обновления по этому поводу? У меня такая же проблема. touchExtend : false не помогает. Проблема возникает, когда я ухожу от представления, где у меня есть карта (в этот момент она уничтожается путем вызова map.remove ()), а затем я возвращаюсь к этому представлению. Он должен создать и инициализировать новую карту, но я получаю ошибку '_leaflet_pos' в getPosition в методе 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)

А также в методе 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)

Та же проблема после map.remove (), переустановите мою карту и получите эту точную ошибку

Та же проблема с v1.6.0. Это сложный вопрос

Вот SSCCE: https://jsfiddle.net/0oafw694/1/

По сути, запуск следующего кода…

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

… Оставляет прикрепленными два слушателя событий:

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 }

Я предполагаю, что zoomanim/_createAnimProxy обрабатывается через unload/_destroyAnimProxy , и поэтому проблем нет. Но moveend/_panInsideMaxBounds нужно отменить. Я подготовлю PR…

Я только что создал div для карты с динамическим идентификатором, поэтому, когда мне нужно повторно использовать div, я удаляю () существующую карту, чтобы освободить память (даже если некоторые события все еще происходят вокруг), а затем перерисуйте div с другим идентификатором, чтобы создать в нем новую карту.

Я также храню все свои карты в объекте, поэтому я могу управлять ими в соответствии с его идентификатором (у меня несколько раз отображается несколько карт, все с динамическими идентификаторами)

Согласно моим экспериментам с листовкой, любые события (например, moveend, movestart и т. Д.), Которыми манипулируют разработчики, изменяют свое поведение по умолчанию и остаются в памяти при выгрузке листовки из dom.
Я сделал это: @moveend="()=>{enableRecenter = true}" и поэтому обработчик 'moveend' остался в памяти при выгрузке / удалении карты.
Я удалил манипуляции (мои собственные реализации) этих методов из самого компонента карты, и теперь эта ошибка перестала появляться.

В общем, НИКОГДА НЕ ПРИКАСАЙТЕСЬ К МЕТОДАМ КАРТЫ !!! Конечно, если только библиотека не обнаружит такое поведение и не исправит эту ошибку.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги