下面我来叙述一个场景,你的用户们由于各种原因,使用鼠标在几秒钟不停的按了数十次,不停点击发起请求,身为前端开发,你不担心你的页面崩溃嘛?!!肯定担心!!!
由此, 就到了我们的性能优化环节——防抖。
一、何为防抖?
概念:任务频繁触发的情况下,只有任务触发的间隔超过制定的时间间隔的时候,任务才会被执行。
防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行,真是任性呐!
除了
mousedown
,还有一些在前端开发中会遇到的频繁的事件触发也需要优化,比如:window.resize
,window.scroll
,mousemove
,keyup
,keydown
……
二、学会防抖!
1. 首先,我们来举个示例代码,来了解事件如何频繁的触发
//有问题的版本
CSS:
#container{width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;}
HTML:
<div id="container"></div>
<script src="debounce.js"></script>
JS:
var count = 1;
var container = document.getElementById('container');
function getUserAction() {
container.innerHTML = count++;
};
container.onmousemove = getUserAction;
我们来看看效果:
从左边滑到右边就触发了 165 次 getUserAction 函数!
因为这个例子很简单,所以浏览器完全反应的过来,可是如果是复杂的回调函数或是 ajax 请求呢?假设 1 秒触发了 60 次,每个回调就必须在 1000 / 60 = 16.67ms 内完成,否则就会有卡顿出现。
为了解决这个问题,一般有两种解决方案:
- debounce 防抖
- throttle 节流
2. 回忆一下,防抖的原理:触发高频函数事件后,n秒内函数只能执行一次,如果在n秒内这个事件再次被触发的话,那么会重新计算时间,要等你触发完事件 n 秒内不再触发事件,才执行。
你肯定想,根据这个原理,写个防抖不是小case?然后唰唰唰一段类似下面的代码就敲出来了!
//第一次更改的初级版本
function debounce(func, wait) {
var timeout;
return function () {
clearTimeout(timeout)
timeout = setTimeout(func, wait);
}
}
//以刚才的165例子,试试这个'防抖'
container.onmousemove = debounce(getUserAction, 1000);
//现在随你怎么移动,反正你移动完 1000ms 内不再触发,我才执行事件。
我们再来看看效果:
顿时就从 165 次降低成了 1 次!!!(棒棒哒,我们接着研究它。)
3. 有一个问题,不知阁下发现了没有,this指向问题来了!!!
如果我们在 getUserAction 函数中 console.log(this)
,在不使用 debounce 函数的时候,this
的值为:<div id="container"></div>
。
但是,注意:如果使用我们的 debounce 函数,this
就会指向 Window
对象!
所以我们需要将 this 指向正确的对象,来吧,改代码咯~
//第二次更改this指向的版本
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context)
}, wait);
}
}
4. this 指向问题已经解决,但是,我们怎么可以忘记对象呢?!!事件处理函数中会提供事件对象 event,对象event有没有问题呢 ?有啥问题?一起来看看吧!
//我们修改下 getUserAction 函数:
function getUserAction(e) {
console.log(e);
container.innerHTML = count++;
};
//如果我们不使用 debouce 函数,这里打印 MouseEvent 对象:
MouseEvent { isTrusted:true,screenX:572,screenY:223,clientX:572,clientY:126...}
//但是在我们实现的 debounce 函数中,却只会打印 undefined!
所以我们再修改一下代码:
//第三次更改event对象的版本
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
到此为止,我们修复了两个小问题:
- this 指向
- event 对象
5. 这个时候,代码已经很是完善了,但是为了让这个函数更加完善,我们接下来思考一个新的需求:
我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
想想这个需求也是很有道理的嘛,那我们加个 immediate
参数判断是否是立刻执行。
//第四次更改立刻执行的版本
function debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
}
完成,我们再来看看效果:
6. 问题来咯,注意一点:就是 getUserAction 函数可能是有返回值的,所以我们也要返回函数的执行结果,但是当 immediate
为 false
的时候,因为使用了 setTimeout
,我们将 func.apply(context, args)
的返回值赋给变量,最后再 return
的时候,值将会一直是 undefined
,所以我们只在 immediate
为 true
的时候返回函数的执行结果。
//第五次更改返回值问题的版本
function debounce(func, wait, immediate) {
var timeout, result;
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
}
}
7. 到此刻,只需再考虑最后一个实际需求,我们就能完成最后版本的防抖代码了。这个小需求是:我希望能取消 debounce 函数,但是,比如说我 debounce 的时间间隔是 10 秒钟,immediate
为 true
,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行啦,是不是很开心?
//第六次更改取消问题的版本。 最后版本
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
};
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
那么该如何使用这个 cancel 函数呢?依然是以最最最上面的 demo 为例:
var count = 1;
var container = document.getElementById('container');
function getUserAction(e) {
container.innerHTML = count++;
};
var setUseAction = debounce(getUserAction, 10000, true);
container.onmousemove = setUseAction;
document.getElementById("button").addEventListener('click', function(){
setUseAction.cancel();
})
一起看看最终效果把:
终于,我们已经完整实现了一个 underscore 中的 debounce 函数,恭喜,撒花!
那么,underscore
又是啥呢?
JavaScript是函数式编程语言,支持高阶函数和闭包。函数式编程非常强大,可以写出非常简洁的代码。例如Array的map()和filter()方法,但是,Array有map()和filter()方法,可是Object没有这些方法。此外,低版本的浏览器例如IE6~8也没有这些方法,怎么办?
方法一,自己把这些方法添加到Array.prototype中,然后给Object.prototype也加上mapObject()等类似的方法。
方法二,直接找一个成熟可靠的第三方开源库,使用统一的函数来实现map()、filter()这些操作。
采用方法二,选择的第三方库就是underscore
。
正如jQuery统一了不同浏览器之间的DOM操作的差异,让我们可以简单地对DOM进行操作,underscore则提供了一套完善的函数式编程的接口,让我们更方便地在JavaScript中实现函数式编程。
jQuery
在加载时,会把自身绑定到唯一的全局变量$
上,underscore
与其类似,会把自身绑定到唯一的全局变量_
上,这也是为啥它的名字叫underscore的原因。