Leaflet: map 没有在 map.remove() 上从 DOM 中完全删除

创建于 2017-09-13  ·  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 :

在此示例中,删除地图后我看不到任何错误,因此显然还有更多原因导致您看到的问题。 能否请您提供更多详细信息,以便我们进行跟踪。

@perliedman感谢您的快速响应并将游乐场示例放在一起。 我无法在那个上重现。 尝试更多场景。 虽然我还在研究它,但有几个问题——

如果我在做map.remove() ,我是否需要显式删除图层? 我的猜测是,它也会处理层去除,但你能确认一下。

并且 map.remove() 超时的原因是在示例中您在创建后立即销毁地图,否则不需要用超时括起来。 正确的?

请记住,在使用开源时,_使用_源! :wink: 要回答你的第一个问题,_yes_, remove将删除图层: https :

我把它放在超时的原因是让我的例子更现实一点,确保瓷砖层实际上用加载的瓷砖等进行了正确初始化。这个例子在没有放入remove调用的情况下也能正常工作超时,但这似乎有点人为的测试。

您好 - 我想我也遇到了这个问题。 这是我的基本用例:

我正在为具有多个页面/表面的对象构建一个查看器组件(使用Leaflet-IIIF插件,但我认为这不会影响这里的任何内容),而不是显示实际地图。 当查看器加载时,用户可以单击一系列缩略图来更新在 UI 的中央区域中显示的对象视图。

当用户更改视图时,我会在为新视图设置新地图之前调用map.remove() 。 新地图是在与旧地图相同的 DOM 元素上创建的(带有 ID 的 div),并且我不会在 Leaflet 之外以任何方式修改 DOM。

在初始视图中,一切正常。 但是在调用map.remove()并显示一个新视图后,控制台会抱怨:每当地图被拖动或缩放时,控制台都会抱怨: Cannot read property '_leaflet_pos' of undefined

我可以尝试在某个时候发布一个最小的例子,但这似乎是同样的问题。 这个错误在 Chrome 中出现,但在 Firefox 中没有。

@egardner是的,请尝试创建一个重现此问题的示例!

@egardner在以前稳定的电子(Chromium + Node)应用程序上完全相同的问题我不得不恢复到:
"传单": "1.0.0",
"leaflet.markercluster": "1.0.0-rc.1.0"
从 1.2.0 删除错误无法读取未定义的属性“_leaflet_pos”
这也是在 map.remove() 之后,然后在同一 DOM 元素中重新创建地图。 我现在没有时间在短期内创造和示范

我们在尝试销毁地图时也遇到了类似的问题,它似乎在保留引用

我将重申我上面所说的:为了让我们能够对此问题做任何事情,请提供一个重现问题的示例。

你好。 我在小提琴中重现了这个错误。 简单地说,如果你在一个 div 元素中创建一个地图,然后使用 remove 方法,然后在同一个 div 上重新填充地图,那么每次地图移动都会产生一个错误
未捕获的类型错误:无法读取未定义的属性“_leaflet_pos”。

要重现,请打开我的小提琴,单击移除地图,单击放置地图,然后打开控制台并移动地图。
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.31.2.0 ),认为我们至少可以将其锁定到特定的1.x.x版本,但没有运气。

有任何更新吗? 这里有同样的问题,将传单集成到我的应用程序中。 地图被销毁后,dom 元素仍然具有 _leaflet_events 属性,但摆脱这个对象也无济于事。

处理程序函数的上下文似乎已经过时(处理程序 args 中的 _mapPane 私有属性指向一个不存在的元素)。

我也在经历这个。 这是我看到的抛出的异常之一:

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

似乎这主要发生在用户使用浏览器后退按钮离开地图时。

我想我可能已经找到了解决方案:

即使在map.offmap.remove之后,Map 容器 div 仍然有一些事件会被触发。

在我的情况下,地图具有以_leaflet_开头的属性,我发现其中一些函数位于地图本身的属性"map._leaflet_events".

这些似乎附加为pointerdownpointermove等,但属性的名称类似于map._leaflet_touchstarttouchstart32等。

我发现如果我迭代这些并手动删除它们(使用removeEventListener然后清零并删除属性本身以获得良好的度量),我可以将 div 重用于另一个地图。
这也结束了我看到的内存泄漏。

我无法在此处发布代码,但是如果您搜索POINTER_DOWN的传单源,您将看到附加的事件,并知道如何分离它们。

我可以在 Chrome 和 FF 上重现这个(在我自己的项目和@spydmobile 提供的 jsfiddle 中)

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

啊三重反引号,明白了,ty。

@FLoibl这是一项非常好的调查:+1:

您能否添加一些日志记录...? https://github.com/Leaflet/Leaflet/blob/5161140e952969c5da27751b79154a2c93f53bfa/src/dom/DomEvent.Pointer.js#L39https://github.com/Leaflet/Leaflet/blob/fe9e0dom82b30d/fe9e080f230dc/fe9e080b30e3c930d/fe9e080fc93f53bfa/src/dom/DomEvent.Pointer.js#L39 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:

我现在只是没有看到地图窗格中有什么指针事件处理程序,但是:思考:

我认为通过不删除地图窗格,您只会掩盖问题。 当我跟踪调用堆栈时,问题是一些 _mapPane 对象指向一个被破坏的 dom 元素,因此试图从未定义的位置获取缓存位置。 如果窗格没有被销毁,幽灵事件可能会通过而不会触发异常。

@Floibl我同意你的看法,我认为这不是解决方案,但我在检查调用堆栈时注意到 null _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);
如果我理解的够多的话,这个timeout在map被移除的时候不会被破坏,所以Map._onZoomTransitionEnd函数总是被调用。 可能是您的_“此处或那里缺少函数调用”_。

而简化的调用树this._onZoomTransitionEnd -> this._move -> this._getNewPixelOrigin -> this._getMapPanePos -> getPosition(this._mapPane) -> return el._leaflet_pos失败,因为this._mapPane是 _undefined_。

也许这种情况可以解决,如果您将this._movethis._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() 被销毁),然后我导航回该视图时,就会出现问题。 它应该创建并初始化新地图,但我在setMaxBounds方法中的getPosition处收到“_leaflet_pos”错误:

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…

我刚刚为具有动态 id 的地图创建了一个 div,所以当我必须重用 div 时,我 remove() 现有地图以释放内存(即使有一些事件仍在发生),然后用不同的 id 重绘 div,所以我在其中创建了一个新地图。

我还将所有地图存储在一个对象中,因此我可以根据其 id 操作它们(有时我有不止一张地图可见,所有地图都具有动态 ID)

根据我对传单的实验,开发人员操作的任何事件(例如 moveend、movestart 等)都会更改其默认行为并在从 dom 卸载传单时保留在内存中。
我这样做了: @moveend="()=>{enableRecenter = true}"等等,'moveend' 的处理程序在卸载/移除地图时保留在内存中。
我从地图组件本身删除了这些方法的操作(我自己的实现),现在这个错误不再出现。

所以基本上,永远不要触摸地图方法!!! 当然,除非库检测到此行为并修复此错误。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

CallMarl picture CallMarl  ·  3评论

zdila picture zdila  ·  3评论

gdbd picture gdbd  ·  3评论

walterfn2 picture walterfn2  ·  4评论

viswaug picture viswaug  ·  4评论