为什么要开发这样一个组件库?
这个想法来源于之前开发的一个项目,该项目需要在 zoom 16 的级别下渲染 (100 * 100) 的小方格,使用高德地图的多边形覆盖物 Polygon 进行渲染,在 mac 13 寸屏幕下渲染 1k+,在外接 27 寸(不太记得多少寸了)显示屏下需要渲染近 3K 的覆盖物。
我尝试使用 vue-amap 这个组件库,在 1k 覆盖物的情况下需要渲染 5 秒左右,在 3k 覆盖物的渲染下会渲染 30+ 秒,甚至会让浏览器直接崩溃。我选择自己通过 AMap SDK 封装了一个组件,然而性能比 vue-amap 还要差,1k 覆盖物需要耗费 10+ 秒,如果我拿这个交给产品,他估计会打死我。
这里有一个渲染 2000 个覆盖物 fast-amap 与 vue-amap 的对比,可以感受一下。
FastAMap codepen.io/taoxusheng/…
VueAMap codepen.io/taoxusheng/…
为什么在 Vue 中使用高德 SDK 有明显性能问题?
事实上,我们在使用 Vue 开发的时候通过props将数据传递给组件或是data,而 Vue 默认会对这些数据进行deepWatch,而我放在 data 上的 Polygon 实例每次都会被 Vue 绑定,这就是造成性能降低的原因。最后我自己封装了一个 Polygon 的渲染类,1k+覆盖物渲染在 1 秒左右,虽然解决了性能问题,但使用却很不方便,因为在业务中有太多关于渲染处理的代码,无法做到只关心数据问题,需要编写很多配置属性。
FastAMap 是如何解决这个问题的?
在那个项目结束之后我就想封装一个组件,然而却一直有些问题困扰着我。
- 如果解决数据被 Vue 绑定的问题?
- 地图加载可能是异步的,如果保证使用子组件的时候地图实例已经加载完成?
- 一个页面中可能有多个地图,以及多个地图相关的子组件,子组件如何获取地图实例,以及如何保证他们的实例是正确的?
数据解耦
我们通过 props 传递的数据都会被 Vue 绑定,但我们可以通过 clone 一份数据。在组件中 watch 数据源,一旦数据变更就创建对应的实例,并将其放入一个不会被 watch 的数组中。
{
watch: {
options: {
immediate: true,
handler: 'handleOptionsChange'
}
},
created() {
mapOptionLoader().then(AMap => {
AMapInstance = AMap
})
// 由于需要将高德地图与 vue 解耦,所以这里创建的实例数组不能被 vue watch。
if (!this.instanceList) {
this.instanceList = []
}
}
methods: {
handleOptionsChange(options) {
this.rendered = false
this.getAMapPromise().then(() => {
// 清除上一次的实例
this.clearAll()
// 获取对应的地图实例
const map = this.getMapInstance(this.mid)
options.forEach(option => {
// 调用组件的创建实例方法
const instance = this.createInstance(option)
this.instanceList.push(instance)
})
this.$nextTick(() => {
this.addPlugins()
})
map.add(this.instanceList)
})
},
}
}
v-if slot
在地图组件中添加一个boolean类型的值mapLoaded,当地图加载完成之后才去渲染子组件。这时候子组件的 mounted 函数中就可以获取到地图实例。
<div ref="container" class="cpt-fast-map" :style="{ height: height + 'px' }">
<div class="fast-map-slot-container">
<slot v-if="mapLoaded"></slot>
</div>
</div>
// js
{
mounted() {
this.getAMapPromise()
.then(AMap => {
let map = null
const options = this.createMapOptions()
try {
map = new AMap.Map(this.$refs.container, options)
} catch (e) {
console.error(e)
}
if (map) {
// 加入地图实例注册表
this.$_amapMixin_setMapInstance(this.mid, map)
// 绑定用户自定义注册事件
this.$_amapMixin_addEvents(map, events)
}
})
.catch(noop)
}
methods: {
// 地图实例中注册的 complete 事件,触发该事件表示地图加载完成。
handleCompleteEvent(event) {
this.mapLoaded = true
this.$emit(event.type, event, this.getMapInstance(this.mid))
},
}
}
图实例注册表
在 AMap 中封装了一个地图的注册表类,当地图创建成功后将实例添加进注册表,销毁后删除注册表中的实例。而所有的地图组件都需要添加一个注册表的 ID,这样就能保证每个组件都能获取到其对应的地图实例了。
import Map from './map-shim'
/**
* 高德地图实例注册表
*/
export default class MapRegistry {
constructor() {
this.registry = null
}
setMap(mid, instance) {
if (!mid) {
warn('The parameter mid cannot be empty')
}
if (this.map) {
if (this.map.get(mid)) {
warn(`mid: ${mid} already exists in the map registry`)
} else {
this.map.set(mid, instance)
}
}
}
getMap(mid) {
return this.map && this.map.get(mid)
}
deleteMap(mid) {
if (this.getMap(mid)) {
this.map.delete(mid)
} else {
warn(`No instance of mid: ${mid} found in the map registry`)
}
}
static getRegistryInstance() {
if (!this.registry) {
this.registry = new MapRegistry()
this.registry.map = new Map()
}
return this.registry
}
}
项目地址与文档地址:
github: link.zhihu.com/?target=htt… 文档: txs1992.github.io/fast-amap/