一個高效能的 Vue 高德地圖元件庫

txs1992發表於2020-01-15

為什麼要開發這樣一個元件庫?

這個想法來源於之前開發的一個專案,該專案需要在 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 是如何解決這個問題的?

在那個專案結束之後我就想封裝一個元件,然而卻一直有些問題困擾著我。

  1. 如果解決資料被 Vue 繫結的問題?
  2. 地圖載入可能是非同步的,如果保證使用子元件的時候地圖例項已經載入完成?
  3. 一個頁面中可能有多個地圖,以及多個地圖相關的子元件,子元件如何獲取地圖例項,以及如何保證他們的例項是正確的?

資料解耦

我們通過 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/

相關文章