為什麼要開發這樣一個元件庫?
這個想法來源於之前開發的一個專案,該專案需要在 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/