从地图类型切换控件谈JS代码优化

本文以 JavaScript 开发自定义百度地图类型切换控件为主线,记录了控件从实现到一步步优化过程中的思考与总结,其中不少关于 JavaScript 代码优化的 tip 在很多场合都很实用。主要知识点包括:HTML 与 CSS 之间的松耦合、JS 的事件委托、HTML 自定义特性、DOM 节点访问及遍历、JQuery 常用方法的使用及百度地图 API 的调用等。这些都是比较基础的知识点,在此尽可能完整的记录,以便今后查阅及完善。

一、实现

1. 百度 API 内部实现

百度地图 JS 版本 API 引入及地图初始化可以参考百度地图API示例 ,在此不再赘述,给出基本的地图展示及内置地图类型切换控件的代码,如下:

(function() {
    // 地图初始化
  var map = new BMap.Map('map');
  map.centerAndZoom(new BMap.Point(116.404, 39.915), 13);
  // 添加地图切换控件
  map.addControl(new BMap.MapTypeControl({
    mapTypes: [
      BMAP_NORMAL_MAP,
      BMAP_HYBRID_MAP
    ]}));
})();

实现效果如下:

image

虽然可以实现基本的地图类型切换,但关于自定义样式和控件显示位置上有很多限制,因此以下提供地图类型切换控件的自定义实现。

2. 自定义实现

自定义实现将新增一个 div 控件元素,并将其添加到地图之上,自定义控件包括「地图」、「卫星」、「混合」三种地图类型的切换。

HTML 代码:

<div id="map-control">
  <div id="ctrl1">地图</div>
  <div id="ctrl2">卫星</div>
  <div id="ctrl3">混合</div>
</div>

CSS 样式:

#map-control {
  width: 120px;
  height: 30px;
  line-height: 30px;
  display: flex;
  position: absolute;
  top: 10px;
  right: 10px;
  border: 1px solid #8EA8E0;
  border-radius: 4px;
  background-color: #FFF;
}

#map-control > div {
  flex: 1;
  text-align: center;
}

#ctrl1 {
  color: #FFF;
  background-color: #8EA8E0;
}

map-control 采用 flext 的布局,让三个子 div 横向均匀分布;另外,position 属性采用 absolute,使其位于地图上方。在此,将 ctrl1 块的样式初始化为激活状态。第 1 版的 JS 代码如下

var ctrl1 = document.getElementById('ctrl1');
var ctrl2 = document.getElementById('ctrl2');
var ctrl3 = document.getElementById('ctrl3');

/**
* Version 1
* 为三个子元素分别添加 DOM2 级事件处理程序,处理样式及地图切换
*/
ctrl1.addEventListener('click', function() {
  // 去除其他两个子元素的激活样式
  ctrl2.style.color = '#000';
  ctrl2.style.backgroundColor = '#FFF';
  ctrl3.style.color = '#000';
  ctrl3.style.backgroundColor = '#FFF';
  // 为当前元素添加激活样式
  ctrl1.style.color = '#FFF';
  ctrl1.style.backgroundColor = '#8EA8E0';
  // 地图类型切换
  map.setMapType(BMAP_NORMAL_MAP);
});

ctrl2.addEventListener('click', function() {
  // 去除其他两个子元素的激活样式
  ctrl1.style.color = '#000';
  ctrl1.style.backgroundColor = '#FFF';
  ctrl3.style.color = '#000';
  ctrl3.style.backgroundColor = '#FFF';
  // 为当前元素添加激活样式
  ctrl2.style.color = '#FFF';
  ctrl2.style.backgroundColor = '#8EA8E0';
  // 地图类型切换
  map.setMapType(BMAP_SATELLITE_MAP);
});

ctrl3.addEventListener('click', function() {
  // 去除其他两个子元素的激活样式
  ctrl1.style.color = '#000';
  ctrl1.style.backgroundColor = '#FFF';
  ctrl2.style.color = '#000';
  ctrl2.style.backgroundColor = '#FFF';
  // 为当前元素添加激活样式
  ctrl3.style.color = '#FFF';
  ctrl3.style.backgroundColor = '#8EA8E0';
  // 地图类型切换
  map.setMapType(BMAP_HYBRID_MAP);
});

上述程序完全能够实现地图类型的切换效果,但是存在很多问题:

  • HTML 与 CSS 耦合严重。利用 JS 对每个元素的样式进行直接修改是很不可取的,一方面导致代码冗余,另一方面如果有一处需要修改,那其余地方都得修改。(如现在需要将元素激活时的背景元素从蓝色改变为绿色,那么就得修改三个事件处理程序的语句)。
  • 事件处理程序繁多,扩展性差。可以看到,上述每一个子元素都添加了一个事件处理程序,代码复用性差的同时,也提高了程序运行时的内存占用。
  • 其他问题下文论述。

针对以上实现存在的问题,以下提出逐步改进方案。

二、改进

1. 降低 HTML 与 CSS 的耦合

低耦合是软件设计的基本原则,为了降低 HTML 和 CSS 的耦合,我们引入一个新的 CSS 类:current,它表示当前选中元素的样式:

#map-control .current {
  color: #FFF;
  background-color: #8EA8E0;
}

第 2 版 JS 代码如下

/**
* Version 2
* 利用 current 样式类,降低 HTML 和 CSS 的耦合
*/
ctrl1.addEventListener('click', function() {
  ctrl2.classList.remove('current');
  ctrl3.classList.remove('current');
  ctrl1.classList.add('current');
  map.setMapType(BMAP_NORMAL_MAP);
});

ctrl2.addEventListener('click', function() {
  ctrl1.classList.remove('current');
  ctrl3.classList.remove('current');
  ctrl2.classList.add('current');
  map.setMapType(BMAP_SATELLITE_MAP);
});

ctrl3.addEventListener('click', function() {
  ctrl1.classList.remove('current');
  ctrl2.classList.remove('current');
  ctrl3.classList.add('current');
  map.setMapType(BMAP_HYBRID_MAP);
});

通过以上改进,我们将样式与元素相分离,若要修改激活元素的样式,只需要修改 current 样式类即可。

Tip 1 解耦 HTML/CSS

在使用 JavaScript 修改元素样式的时候,尽量修改元素的样式类,而不是直接修改样式本身。

2. 使用事件委托

以上代码还可以进一步改进。我们知道在 DOM 事件冒泡的过程中,事件的触发是从当前元素逐级往上传递,因此当我们需要监听很多子元素事件的时候,实际上只监听其父元素的事件即可,当然需要在父元素的事件处理程序中对当前点击的子元素进行具体的判断。这样一来可以减少事件处理程序的数量,提供代码复用和内存利用率。这种方法就叫作事件委托。

第 3 版 js 代码如下

/**
* Version 3
* 使用事件委托,减少事件处理程序数目
*/
var mapControl = document.getElementById('map-control');
mapControl.addEventListener('click', function(event) {
  var child = this.firstElementChild;
  while(child !== this.lastElementChild) {
    child.classList.remove('current');
    child = child.nextElementSibling;
  }
  child.classList.remove('current');
  event.target.classList.add('current');
  switch(event.target.id) {
    case 'ctrl1': {
      map.setMapType(BMAP_NORMAL_MAP);
      break;
    }
    case 'ctrl2': {
      map.setMapType(BMAP_SATELLITE_MAP);
      break;
    }
    case 'ctrl3': {
      map.setMapType(BMAP_HYBRID_MAP);
      break;
    }
  }
});

第 3 版代码看起来比第 2 版还要复杂,但它的思路是非常简单的。在父元素 mapControl 添加一个事件处理函数,该函数有一个事件对象 event,它记录当前被点击元素的一些属性以及本次事件的一些属性,利用我们可以通过 event.target 获取到当前点击的元素。在事件处理函数内部,我们首先遍历了 mapControl 的子元素,并将它们的样式类 current 都移除掉(原生 JS 遍历确实有点麻烦,在这里用 do-while 循环应该更好),然后通过 event.target 获取当前点击的元素,并将样式类 current 添加到该元素,以上完成了点击时的样式切换。

接下来是点击后的地图类型切换了,在此利用 event.target.id 属性确定当前点击的是哪一个元素,然后再设置对应的地图类型。总的来说,利用事件委托可以将事件处理程序的数目降到最少,提高代码复用。

Tip 2 使用事件委托

如果要为多个并列的元素分别添加类似的事件处理程序,可以考虑利用事件委托,将事件处理程序添加到这些并列元素的父元素上。

3. 自定义 HTML 特性

以上 switch 语句看起来很不优雅,代码量很大,看起来很尴尬。为此我们引入一个 mapTypeArr 的数组,并为 mapControl 元素下的每一个 div 添加一个自定义属性,从而不再需要 id 属性。

更新后的 HTML 代码如下:

<div id="map-control">
  <div data-maptype="0" class="current">地图</div>
  <div data-maptype="1">卫星</div>
  <div data-maptype="2">混合</div>
</div>

第 4 版 js 代码如下

/**
* Version 4
* 利用 HTML 自定义特性,避免 switch
 */
var mapControl = document.getElementById('map-control');
var mapTypeArr = [BMAP_NORMAL_MAP, BMAP_SATELLITE_MAP, BMAP_HYBRID_MAP];
mapControl.addEventListener('click', function(event) {
  var child = this.firstElementChild;
  var target = event.target;
  while(child !== this.lastElementChild) {
    child.classList.remove('current');
    child = child.nextElementSibling;
  }
  child.classList.remove('current');
  target.classList.add('current');

  map.setMapType(mapTypeArr[parseInt(target.getAttribute('data-maptype'))]);
});

第 4 版代码同第 3 版相比,通过自定义的 HTML 特性和一个 mapTypeArr 数组,优化了 setMapType() 相关语句。

Tip3 HTML 自定义特性

我们可以自定义 HTML 元素的特性,自定义的特性一般以 data- 开头,统一采用小写。原生 DOM 元素的 getAttribute() 方法也能获取元素的自定义特性。

另外可以注意到一个细节,event.target 使用了多次,为了提高程序性能,我们用了一个局部变量 target 将 event.target 保存起来,避免属性的全局查找。

Tip4 避免属性的全局查找

如果经常需要用到元素的某一个属性,为了避免每一次调用时都进行一次查找,可以用一个局部变量将该属性进行缓存。避免使用 with,因为 with 会加长作用域链,使得属性的查找变慢。

4. 使用 JQuery 改进元素查找

从第 4 版 js 代码可以看到,程序依然比较冗长的原因主要是在元素的遍历部分。为此,我们使用 JQuery 改进元素的查找。

第 5 版 js 代码如下

/**
* Version 5
* 使用 jquery 改进元素查找
*/
var $mapControl = $('#map-control');
var mapTypeArr = [BMAP_NORMAL_MAP, BMAP_SATELLITE_MAP, BMAP_HYBRID_MAP];
$mapControl.on('click', function(event) {
  var $target = $(event.target);
  $target.addClass('current').siblings('div').removeClass('current');
  map.setMapType(mapTypeArr[$target.attr('data-maptype')]);
});

使用了 JQuery 后,地图类型切换控件的实现变得非常的精简。通过事件对象 event 获取当前当前的元素,并将其转换为 JQuery 元素对象,并利用 JQuery 的链式编程方法,用一条语句激活当前元素的样式,并去除其他元素的样式。最后设置地图类型。虽然 JQuery 性能不一定比得上原生 JS,但可以极大的简化代码量,这在很多场合是非常用帮助的。

Tip5 合理使用 JQuery 可以极大精简你的代码

三、总结

以上便是「从地图类型切换控件谈 JS 代码优化」的全部内容,后续如果有更加简单高效的实现方式,再进行补充,也欢迎大家提出自己的思路。以下将优化过程中用到的 Tip 进行总结:

  • Tip 1 解耦 HTML/CSS

    在使用 JavaScript 修改元素样式的时候,尽量修改元素的样式类,而不是直接修改样式本身。

  • Tip 2 使用事件委托

    如果要为多个并列的元素分别添加类似的事件处理程序,可以考虑利用事件委托,将事件处理程序添加到这些并列元素的父元素上。

  • Tip3 HTML 自定义特性

    我们可以自定义 HTML 元素的特性,自定义的特性一般以 data- 开头,统一采用小写。原生 DOM 元素的 getAttribute() 方法也能获取元素的自定义特性。

  • Tip4 避免属性的全局查找

    如果经常需要用到元素的某一个属性,为了避免每一次调用时都进行一次查找,可以用一个局部变量将该属性进行缓存。避免使用 with,因为 with 会加长作用域链,使得属性的查找变慢。

  • Tip5 合理使用 JQuery 可以极大精简你的代码


原文地址:http://nightn.com/2017/11/28/js-optimize-maptype-demo

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容