浅谈函数节流与防抖

概述

函数节流(throttle)的核心是,让一个函数不要执行得太频繁,减少一些过快的调用来节流。
函数去抖(debounce)就是对于一定时间段的连续的函数调用,只让其执行一次。

应用场景

throttle 和 debounce 的应用场景应该是分的很清楚的

1.按一个按钮发送 AJAX:给 click 加了 debounce 后就算用户不停地点这个按钮,也只会最终发送一次;如果是 throttle 就会间隔发送几次
2.监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次

函数去抖示例

以 scroll 事件为例,探究如何实现滚动一次窗口打印一个 hello world 字符串。
如果不对其节流或者去抖控制

1
2
3
window.onscroll = function() {
console.log('hello world');
};

这样每滚动一次,实际上会打印 N 多个 hello world。函数去抖背后的基本思想是指,某些代码不可以在没有间断的情况连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。

《高程三》给出了最简洁最经典的去抖代码(书中说是节流,实则为去抖),调用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function debounce(method, context) {
clearTimeout(method.iTd)
method.iTd = setimeout(function(){
method.call(context)
}, 1000)
}

function print () {
console.log("hello world")
}

window.onscroll = function () {
debounce(print)
}

在窗口内滚动一次,停止,1000ms 后,打印了 hello world,因为我们设置了一个 1000ms 延迟的定时器,细思非常巧妙。

看官观察下另一个版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function debounce(method, context) {
var timer = null
return function () {
clearTimeout(timer)
timer = setimeout(function(){
method.call(context)
}, 1000)
}

}
function print () {
console.log("hello world")
}

window.onscroll = function () {
debounce(print)
}

从这个意义上讲,闭包其实就是用来将两个内容隔离用的,将 timer 放入函数中,那么就需要将原来的语句放入函数中,使其与 timer 隔离,最近返回这个函数。结果就会和原来的效果是一样的。

underscore 在其基础上进行了扩充,直接看代码,含大量注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
_.debounce = function (func, wait, immediate) {
var timeout, args, context, timestamp, result
var later = function () {
var last = _.now() - timestamp;
if (last < wait && last >= 0) {
timeout = seTimeout(later, wait - last)
}else {
timeout = null
if (!immediate){
result = func.apply(context, args)
if (!timeout) {
context = args = null
}
}
}
}
return function() {
context = this;
args = arguments;
timestamp = _.now();
var callNow = immediate %% !timeout
if (!timeout) {
timeout = seTimeout(later, wait)
}
if (callaNow) {
result = func.apply(context, args)
context = args = null
}
return result
}
}

当然,除了功能上的优势,性能也是提高不少,最显而易见的是基础版每此触发事件都会取消定时器,然后重新设置定时器,而 underscore 中会在一定时间后才取消定时器,重新设置定时器

函数节流示例

某些代码不可以在没有间断的情况连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。

函数节流的基本模式

1
2
3
4
5
6
7
8
9
10
11
12
13
var processor = {
timeoutId : null,
performing: function{
//实际执行代码
},
process: function () {
clearTimeout( this.timeoutId)
var that = this;
this.timeoutId = seTimeout(function(){
that.performing();
},100)
}
}

已常见的搜索功能测试

1
2
3
4
5
6
7
8
9
10
11
HTMl:
<input id="search" type="text" name="search">
JS:
<script>
function queryData(text){
console.log("搜索:" + text);
}
var input = document.getElementById("search");
input.addEventListener("keyup", function(event){ queryData(this.value);
});
</script>

这种情况下,每按下一个键盘键,就输出了一次。短短的一些内容,输出了14次,如果每一次都是一次ajax查询请求的话就发了14个请求了。在性能上的消耗可想而知。

使用函数节流模式的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
HTML:
<input id="search" type="text" name="search">
JS:
<script>
function queryData(text){
console.log("搜索:" + text);
}
var input = document.getElementById("search");
input.addEventListener("keyup", function(event){
throttle(queryData, null, 500, this.value);
// queryData(this.value);
});

function throttle(fn,context,delay,text){
clearTimeout(fn.timeoutId);
fn.timeoutId = setTimeout(function(){
fn.call(context,text);
},delay);
}
</script>

这种情况下,输入了好一些内容,只输出了一次,因为测试的时候设置了两次输入间隔是500ms,实际应用可根据情况设置。显然,这在性能上大大滴得到了优化。不过,这样的话,有一个新问题两次之间的输入间隔都小于自己设置的delay值,那么,这个queryData搜索函数就一直得不到调用。实际上,我们更希望的是,当达到某个时间值时,一定要执行一次这个搜索函数。所以,就有了函数节流的改进模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function queryData(text){
console.log("搜索:" + text);
}
var input = document.getElementById("search");
input.addEventListener("keyup", function(event){
throttle(queryData, null, 500, this.value,1000);
});

function throttle (fn, context, delay, text, mustApplyTime){
clearTimeout(fn.timer);
fn._cur = Date.now(); //记录当前时间

if(!fn._start) { //若该函数是第一次调用,则直接设置_start,即开始时间,为_cur,即此刻的时间
fn._start = fn._cur;
}
if(fn._cur - fn._start > mustApplyTime){
//当前时间与上一次函数被执行的时间作差,与mustApplyTime比较,若大于,则必须执行一次函数,若小于,则重新设置计时器
fn.call (context, text);
fn._start = fn._cur;
}else {
fn.timer = setTimeout(function(){
fn.call(context, text);
},deley);
}
}

更多学习链接

节流与去抖