JavaScript的妙与乐(一)之 函数优化
JavaScript的妙与乐系列文章主要是展示一些JavaScript
上面比较好玩一点的特性和一些有用的技巧,里面很多内容都是我曾经在项目中使用过的一些内容(当然,未必所有技巧的使用频率都很高_)。
本篇文章主要是探讨一些辅助函数的书写优化。
函数优化
在JavaScript
中由于浏览器的兼容性差异,导致了在操纵DOM
相关的代码中常常会有所差异,比如最经典的给DOM绑定事件
//dom
var dom = document.getElementById('example');
//event
function clickEvent () {
console.log('click!');
}
//IE9以上及Chrome、Firefox
dom.addEventListener('click', clickEvent, false);
//IE
dom.attachEvent('onclick', clickEvent);
//全部通用
dom.onclick = clickEvent;
可以看出给DOM绑定事件可以有三种实现方式,而我们会依次采用addEventListener
/attachEvent
/onclick
这样的排序去绑定事件。聪明的我们可能就会立即想到我们应该要用一个通用函数来处理,毕竟谁也不想每次绑定事件都要判断一次浏览器特性。
function BindEvent (dom, event, handle, ex) {
if ('addEventListener' in dom) {
dom.addEventListener(event, handle, ex || false);
} else if ('attachEvent' in dom) {
dom.attachEvent('on' + event, handle);
} else {
dom['on' + event] = handle;
}
}
这看起来非常棒,使用起来也不错。
var dom = document.getElementById('example');
BindEvent(dom, 'click', function () {
alert('hey!');
});
现实情况往往会比想象中复杂那么一点点,页面上不可能只有一两个dom
,在一些SPA
中甚至会成千上万,来让我们尝试一下优化。其实对于浏览器的特性侦测只需要执行一次即可,而没有必要每次绑定事件的时候都去判断,我们不妨将BindEvent
改成立即执行函数。
var BindEvent = (function () {
if ('addEventListener' in document) {
return function (dom, event, handle, ex) {
dom.addEventListener(event, handle, ex || false);
}
} else if ('attachEvent' in document) {
return function (dom, event, handle) {
dom.attachEvent('on' + event, handle);
}
} else {
return function (dom, event, handle) {
dom['on' + event] = handle;
}
}
})();
这样BindEvent
就会在加载的时候直接采用了最符合当前浏览器情况的实现方式。最后让我们来写一些测试代码对比一下效率,对于我们页面上最可能出现的情况(有1000个DOM
)。这里我们采用Chrome提供的console.profile
函数来做性能分析
function GenerateDOM (start, end) {
for (; start < end; start++) {
var dom = document.createElement('div');
dom.id = 'd' + start;
dom.innerHTML = 'I\'m d' + start + ' !';
document.body.appendChild(dom);
}
}
GenerateDOM(0, 1000);
function BindDOMEvent (doms, event, handle, ex) {
for (var i = 0; i < doms.length; i++) {
BindEvent(doms[i], event, handle, ex);
}
}
var doms = document.getElementsByTagName('div');
console.profile();
BindDOMEvent(doms, 'click', function () {
alert('hey! ' + this.id);
});
console.profileEnd();
当你在页面按F12
打开console
点击Profiles
的tab你可以看到Chrome
提供的分析结果。而我们暂时只需要分析BindDOMEvent
的耗时即可。如下图:
以下是我分别各执行两种版本10次后得出的综合分析结果,可以看出避免重复判断后,性能平均快了0.7ms,而且优化后基本能够维持在2.1ms(快了约1.27倍)
原始版本
3.3 / 3.4 / 3.3 / 2.3 / 3.5 / 2.3 / 3.4 / 3.3 / 2.2 / 3.3
avg: 3.03 max: 3.5 min: 2.2
优化后版本
2.1 / 2.1 / 2.1 / 3.4 / 2.1 / 2.2 / 2.2 / 3.2 / 2.2 / 2.1
avg: 2.37 max: 3.4 min: 2.1
所以,我们可以直观的判断出,当有类似重复判断的函数,不妨观察分析一下是否能够将该判断只进行一次。这样已经能够给我们的性能带来一定的提升。
优化之路永无止境
对于大多数的项目来说,类似BindEvent
这样的函数一般都会封装成一个Library以便共用代码。而这样的公用Library常常是每个页面都会默认加载。
<script src="/public/helper.js"></script>
但是实际上每个页面所用到的功能仅仅是Library里面的一小部分,例如我们刚刚所写的BindEvent
。我们回头看一看实现代码。
var BindEvent = (function () {
if ('addEventListener' in document) {
return function (dom, event, handle, ex) {
dom.addEventListener(event, handle, ex || false);
}
} else if ('attachEvent' in document) {
return function (dom, event, handle) {
dom.attachEvent('on' + event, handle);
}
} else {
return function (dom, event, handle) {
dom['on' + event] = handle;
}
}
})();
这里有什么可以优化的点呢?在于我们不确定这个页面是否会用到这个function
的时候,我们都默认帮他初始化好了,这里面会有一定的性能损耗。毕竟一个Library当中这样的方法会有N个,当N个函数都在页面加载的时候进行初始化,对于整个页面加载速度方面的影响不容小瞧。
那怎么去做优化呢?惰性加载,只有当有调用方去触发这个function
的时候我们才去做初始化。实现代码如下:
var BindEvent = function (dom, event, handle, ex) {
if ('addEventListener' in document) {
BindEvent = function (dom, event, handle, ex) {
dom.addEventListener(event, handle, ex || false);
}
} else if ('attachEvent' in document) {
BindEvent = function (dom, event, handle) {
dom.attachEvent('on' + event, handle);
}
} else {
BindEvent = function (dom, event, handle) {
dom['on' + event] = handle;
}
}
BindEvent(dom, event, handle, ex);
};
这样当BindEvent
被调用了一次之后,我们的BindEvent
函数就会被替换成符合浏览器特性的实现方式,之后的所有调用均是调用最优结果。最后我们再进行同样地测试看一下对比
3.2 / 2.1 / 2.2 / 3.2 / 3.2 / 2.1 / 3.1 / 2.1 / 3.2 / 3.2
avg: 2.76 max: 3.2 min: 2.1
可以看到平均耗时是2.75而之前是2.37,性能仅仅是略为减慢而已(约是1.16倍),影响并不是非常大,可是对于页面加载的影响呢?
来,让我们用console.time
来做一下对比测试,看一下两种实现方式对于页面加载的影响。
console.time('event1');
var BindEvent = (function () {
if ('addEventListener' in document) {
return function (dom, event, handle, ex) {
dom.addEventListener(event, handle, ex || false);
}
} else if ('attachEvent' in document) {
return function (dom, event, handle) {
dom.attachEvent('on' + event, handle);
}
} else {
return function (dom, event, handle) {
dom['on' + event] = handle;
}
}
})();
console.timeEnd('event1');
console.time('event2');
var BindEvent2 = function (dom, event, handle, ex) {
if ('addEventListener' in document) {
BindEvent = function (dom, event, handle, ex) {
dom.addEventListener(event, handle, ex || false);
}
} else if ('attachEvent' in document) {
BindEvent = function (dom, event, handle) {
dom.attachEvent('on' + event, handle);
}
} else {
BindEvent = function (dom, event, handle) {
dom['on' + event] = handle;
}
}
BindEvent(dom, event, handle, ex);
};
console.timeEnd('event2');
测试结果中前面的运行毫秒是BindEvent
而后面的则是BindEvent2
0.038 / 0.007
0.041 / 0.006
0.031 / 0.007
0.053 / 0.009
0.041 / 0.007
BindEvent
平均耗时: 0.0408 而 BindEvent2
平均耗时: 0.00702
前者约是后者的5.81倍
通过整体评测可以看得出来对于一个能够采用惰性加载的函数,通过这样的一个方式,能够大大减少页面加载时间,同时不会对执行时的运行效率产生太大的影响。
结论
- 对于需要重复判断的条件看能否提取出来只判断一次
- 如果不能,也可以选择保存一个
bool
值变量来做,而不要进行过于复杂的判断
- 如果不能,也可以选择保存一个
- 对于不是页面必须的函数,可以采用惰性加载方式来实现
怎么样?你学会了这个技巧了么?
console.profile与console.time的文档 需要自备梯子- -!