一个高性能的 Vue 高德地图组件库

3,425 阅读3分钟

为什么要开发这样一个组件库?

这个想法来源于之前开发的一个项目,该项目需要在 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/