这几天翻了下以前写的代码,发现自己以前虽然有用 debounce 或者 throttle 的意识,但是确没有把代码封装的很好。比如没有用到闭包去封装 timer, 而是把 timer 放在了 vue data 的变量里。因此就出现了这篇文章,总结下 debounce 和 throttle 运用场景以及对应的自己的实现。并且重构了以前的代码。
运用场景
比如我曾经在一个项目里,就两种场景都用到了。
第一种比如当用户在搜索框里输入了数据的时候,我会去向后台请求,搜索出来的对应结果。 但是用户的输入可能是不断的进行的,如果我每次都去请求,那么其实是无效并且浪费的。所以我们可以在用户输入最后一个字的时候,再去发请求。 用到的就是 debounce 的原理,每次的请求都延迟一段时间去发出,当有新的请求来的时候,清空上次的请求,然后重新执行延迟一段时间去请求,直到用户没有再请求的时候,执行的就是最后一次延迟请求。
这种只去执行最后一次的就是我们说的 debounce。可以把它理解为独占型的函数
。
第二种比如当用户滚动的时候,当滑到了这个字母开头的时候,提示给用户,你已经到了 H 开头的列表内容了。正常的情况下,我们的做法是一直监听 scroll 事件,然后计算当前的 li 是不是到了对应 H 字母开头了,到了则设置相应的提示。
但是要知道 scroll 事件,每次都会触发很多次,如果每次进行相应的计算,就会很卡顿,尤其在一些老的机器上。 那么这个时候我们就可以用 throttle 了,每一段事件,比如 500 ms 去执行一次计算,而不是每次 scroll 都执行。
可以把 throttle 理解为节制型
的函数。
上面两个是我遇到过的两个场景,具体其他场景还可以参考:
1 | // 当 onresize 的时候,我们只想知道最后一次的大小 |
实现代码
在 underscore lodash 里,都有相应的实现了。但是我感觉不太好理解额,所以按照自己的理解,写出了适应自己的 debounce 和 throttle。 具体代码如下:
1 | /** |
leading 参数用于控制第一次是否需要在最开始就触发一次。主要的思想就是只要重新有调用,就把原来的那个被延迟执行了的方法取消。 取消的方法是设置 timer 为 null。firstInvoke 用于标识方法知否被执行了一次了。
下面是 throttle 的实现。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
32
33
34
35
36
37
38
39
40
41
42/**
* throttle
* @param fn
* @param wait
* @param leading
* @param trailing
* @returns {Function}
*/
function throttle(fn, wait, { leading = true, trailing = true }) {
let context, lastExec = 0, timer, args, firstInvoke = true
function excute() {
fn.apply(context, args)
lastExec = +new Date()
}
return function (...arg) {
let now = +new Date()
args = arg
context = this
clearTimeout(timer)
if (firstInvoke && leading) {
excute()
firstInvoke = false
}
if (!lastExec) {
lastExec = now
}
if (!!wait && (now - lastExec) >= wait) {
excute()
} else if (trailing) {
timer = setTimeout(excute, wait)
}
}
}
throttle 因为是按频率触发,所以每次的时间间隔是相同的。所以当没到该执行的时间点的时候,就把剩余的时间重新设置给 setTimeout 。wait 即为多少秒执行一次的时间。看了 underscroll 里的源码,它还设置了 trailing 这些参数。trailing 表示当最后一次没到执行时间时,你不想要延迟执行这个函数了。也就是最后一次调用将被忽略。
下面贴一下 underscroll 的实现。
1 | // Returns a function, that, when invoked, will only be triggered at most once |
underscore 把 throttle 和 debounce 分开来实现了,但是有些别的封装的库则是用一个函数实现。下面是 underscore debounce 的实现
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
32
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
_.debounce = function(func, wait, immediate) {
var timeout, result;
var later = function(context, args) {
timeout = null;
if (args) result = func.apply(context, args);
};
var debounced = restArguments(function(args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = _.delay(later, wait, this, args);
}
return result;
});
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};
总结
剩下的时间就是去优化代码啦。看了下别人的源码,然后自己又按照自己的方式实现了下,还是很有收获的。
不过,如果 underscore 已经有这么成熟的东西,实际上,我觉得可以不用重复的去做这个工作。用别人成熟的内容就好了。自己了解了原理,自己可以实现,有问题知道怎么去查就好。