从地图类型切换控件谈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
]}));
})();
实现效果如下:
虽然可以实现基本的地图类型切换,但关于自定义样式和控件显示位置上有很多限制,因此以下提供地图类型切换控件的自定义实现。
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 可以极大精简你的代码