背景:

在地图上绘制大量的circleMarker,leaflet能选择使用canvas来渲染,比起默认的svg渲染来说在大量绘制的情况下会更加流畅。但当触发其中某一个circleMarker的tooltip或popup时,浏览器报错“Uncaught RangeError: Maximum call stack size exceeded”:

解决过程:

1. 写了个测试代码来复现问题:

 <!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<title>Add a raster tile source</title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
<script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet-src.js"></script>
<!--<script src="./vue.js"></script>-->
<!--<script src="./leaflet.js"></script>-->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css"
integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ=="
crossorigin=""/>
<style>
* { margin:0; padding:0; }
html,body,#vue-wrap,#map { height: 100%; }
</style>
</head>
<body>
<div id="vue-wrap">
<div id="map">test</div>
</div>
<script>
new Vue({
el: '#vue-wrap',
data: function () {
return {
map: '',
canvas: L.canvas()
};
},
mounted: function () {
this.init();
},
methods: {
init () {
this.map = new L.Map('map', {
center: [39.928953, 116.389129],
zoom: 11,
maxZoom: 18,
attributionControl: false,
zoomControl: true
}); this.paintMarkers();
},
paintMarkers () {
console.log('start paint');
console.time('paint');
for (let i = 0; i < 50000; i++) {
let marker = L.circleMarker(this.generateLatlng(), {
color: '#000',
weight: 1,
opacity: 1,
fillOpacity: 0.8,
radius: 6,
fillColor: 'orange', renderer: this.canvas
});
marker.bindTooltip(i + '');
marker.bindPopup(`i: ${i}`);
this.map.addLayer(marker);
}
console.timeEnd('paint');
},
generateLatlng () {
let lat_min = 39.70111,
lat_max = 40.14660,
lng_min = 116.05843,
lng_max = 116.63521; let lat = this.getRandomNum(lat_min, lat_max),
lng = this.getRandomNum(lng_min, lng_max); return [lat, lng];
},
getRandomNum (min, max) {
max = Math.max(min, max);
min = Math.min(min, max);
return Math.random() * (max - min) + min;
}
}
});
</script> </body>
</html>

绘制50000个circleMarker,当鼠标移动到其中某个marker上时,浏览器报错。

注释第59行的代码,或者把map从vue实例的data里提取出来放在全局都不会爆栈,因此现在有两个问题:

  • 为什么放在全局不会爆栈
  • 为什么svg渲染不会爆栈

2. 问题1肯定和vue.js的observe函数相关,通过查看vue.js的代码发现

vue.js初始化实例时会调用: _init->initState->initData->observe(data),在observe函数里会新建个Observe,标注__ob__属性,如果该值为对象,还会调用walk函数来为对象的所有属性添加Observe。

3. 感觉爆栈可能和这个walk有关,但是要怎么证明呢?修改了一下vue.js源码,每次walk的时候都输出此时的遍历链条,如:{a:{b: c}}遍历到c属性时输出 a->b->c。
关键修改点:

 Observer.prototype.walk = function walk (obj, prefix = '') {        // 添加prefix存储遍历节点
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], undefined, undefined, undefined, `${prefix}->${keys[i]}`);
console.log(`${prefix}->${keys[i]}`);
}
};

同时,observe相关的函数都添加prefix来保存遍历的节点信息

4. 先只绘制5个点来测试一下输出结果

没问题,当把鼠标放在Marker上触发tooltip时,有意思的事情出现了:

注意到_order->prev这个链条很长,observe递归很深。当修改marker绘制数目为50000后,确实是->prev->不断的递归并爆栈

5、查看leaflet代码发现canvas绘制时会为画布上的元素添加_order链表属性来存储画布上所有元素的绘制先后顺序,方便bringToFront、bringToBack之类的方法实现;当模拟事件触发时也是通过这个链表来寻找对应的元素。因此当绘制元素过多时,链表太长,vue的observe不断的递归,造成了爆栈现象

6、那么为什么只有触发tooltip/popup的时候才爆栈呢?

因为map.addLayer(marker)时,能够触发observe的操作在搭建链表关系之前(添加子图层this._layers[id]=layer不能触发observe)。红框部分的push触发了observe

7. 所以把map从vue实例的data中拿出来放在外面,map的属性没有被observe就不存在爆栈的问题了。而svg渲染时不存在这样的链表结构,所以也不会爆栈。

8. 为什么svg不需要这种链表结构?

因为svg可以利用DOM API来实现bringToFront/bringToBack之类的操作,而且事件能直接绑定在dom元素上,也不需要遍历所有元素来判断哪个元素是事件的触发对象。而canvas需要使用事件委托来捕获事件,并遍历所有元素来判断具体哪个元素是事件的触发对象。

解决方案:

  • 把视图无关的属性从data里拿出来,但是这样不太方便mixin,只能考虑做成getter、setter形式。这样也能减少不必要的observe
  • 利用vue的相关api来unwatch相关属性,但目前没找到如何unwatch data的属性

记vue+leaflet的一次canvas渲染爆栈的更多相关文章

  1. 使用 Vue 2.0 实现服务端渲染的 HackerNews

    Vue 2.0 支持服务端渲染 (SSR),并且是流式的,可以做组件级的缓存,这使得极速渲染成为可能.同时, 和 2.0 也都能够配合 SSR 提供同构路由和客户端 state hydration.v ...

  2. Seen.js – 使用 SVG 或者 Canvas 渲染 3D 场景

    Seen.js 渲染3D场景为 SVG 或者 HTML5 画布.Seen.js 包含对于 SVG 和 HTML5 Canvas 元素的图形功能的最简单的抽象.所有这个库的其它组件都是不用关心将要渲染的 ...

  3. Vue.js 系列教程 1:渲染,指令,事件

    原文:intro-to-vue-1-rendering-directives-events 译者:nzbin 如果要我用一句话描述使用 Vue 的经历,我可能会说“它如此合乎常理”或者“它提供给我需要 ...

  4. 【开源】微信小程序、小游戏以及 Web 通用 Canvas 渲染引擎 - Cax

    Cax 小程序.小游戏以及 Web 通用 Canvas 渲染引擎 Github → https://github.com/dntzhang/cax 点我看看 DEMO 小程序 DEMO 正在审核中敬请 ...

  5. vue.js选择if(条件渲染)详解

    vue.js选择if(条件渲染)详解 一.总结 一句话总结: v-if <!DOCTYPE html> <html lang="en"> <head& ...

  6. vue.js循环for(列表渲染)详解

    vue.js循环for(列表渲染)详解 一.总结 一句话总结: v-for <ul id="example-1"> <li v-for="item in ...

  7. SVG和canvas渲染的性能比较

    1.什么是SVG? 描述: 一种使用XML描述的2D图形的语言 SVG基于XML意味着,SVG DOM中的每个元素都是可用的,可以为某个元素附加Javascript事件处理器. 在 SVG 中,每个被 ...

  8. 基于vue+leaflet+echart的足迹分享评论平台

    (其实题目是随便取的,目的只是用来证明Vue+leaflet+springboot技术栈的可行性) 效果 小专栏不支持上传视频?想看的话可以去我的知乎看最新的文章,那个应该可以.在这里 主要功能描述 ...

  9. 在vue中获取不到canvas对象? 两种解决办法。

    1. mounted 钩子函数 初次肯定获取到id 2. 如果canvas父级用到了v-if 请改成v-show ,vue Dom节点 重新渲染导致methods 方法获取不到对象.

随机推荐

  1. 多服务器之间Session共享

    原理:多个服务器间想共享session,就相当于共享取多台主机上的一个变量,所以共享的思路就是让大家都能读取变量,实现的方法可以通过将session保存到专门的一个服务器上,所有服务器都去请求数据,也 ...

  2. 从SNE到t-SNE再到LargeVis

    http://bindog.github.io/blog/2016/06/04/from-sne-to-tsne-to-largevis/

  3. s5_day5作业

    # 1.写函数,用户传入修改的文件名,与要修改的内容,执行函数,完成批量修改操作 # def number_file(file,change_s,change): # import os # with ...

  4. 关于 ActionBar、ToolBar、StatusBar 的开发经验整理

    一.ActionBar.ToolBar概述 1.1 ActionBar 1.2 ToolBar 1.3 StatusBar StatusBar 是一个半透明阴影,View 可以伸展到其后面. 1.4 ...

  5. POJ - 1904 King's Quest (强连通)

    题意:有N个王子,每个王子有任意个喜欢的妹子,巫师会给出一个方案:每个妹子都嫁给一个王子.但是国王希望知道:每个王子能在哪些妹子中择偶而不影响其他王子择偶. 分析:设王子为x部,妹子为y部,假设有匹配 ...

  6. wpf利用线程制作初始界面和关闭窗体特效

    1.首先定义初始窗体,和主窗体. 初始窗体(StartWindow) 主窗体(MainWindow): 2.在主窗体界面中,加载初始窗体.注意在线程中操作UI元素需要使用BeginInvoke或者In ...

  7. python步长为负时的情况

    Sequence[start:end:step] python 的序列切片中,第一个:隔离了 起始索引 和 结束索引,第二个:隔离了 结束索引和 步长 step为正,则从左到右切片,如果 start ...

  8. Linux 上下左右键变成^A,^B,^C,^D解决方法

    用gedit打开 /etc/vim/vimrc.tiny,将里面的 set compatible 改成 set nocompatible 对于退格键backspace的问题,只需在刚才那句话下面加上一 ...

  9. 使用POI做的一个生成Excel的工具类。包含了导出Excel和解析Excel方法

    PoiExcelUtils.java /** * */ package com.common.office; import java.io.File; import java.io.FileInput ...

  10. 20145235李涛《网络对抗》Exp8 Web基础

    基础问答 什么是表单 可以收集用户的信息和反馈意见,是网站管理者与浏览者之间沟通的桥梁. 表单包括两个部分:一部分是HTML源代码用于描述表单(例如,域,标签和用户在页面上看见的按钮),另一部分是脚本 ...