在前端开发的过程中,相信你一定遇到这种情况:自己设定的事件频繁的执行
,体验感极差,添加延时器虽然对有些情况能改善,但作用不大,不仅用户体验感差,性能也很不好,对浏览器增加了极大负担。这种情况该怎么解决呢?对啦,debounce(防抖)和throttle(节流
)就是比较好的解决方案。
首先让我们先来了解一下函数的防抖和节流是什么吧
函数防抖
当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。
简单来说防抖就是:任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。
函数节流
当持续触发事件时,保证一定时间段内只调用一次事件处理函数。也可以理解为连续触发事件但是在 n 秒中只执行一次函数。
比较直白的说,函数节流就是强制规定一个函数在一段时间内能够被执行的最大次数 节流会稀释函数的执行频率。
区别
函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数
再来看看在事件持续触发的过程中频繁执行函数是怎样的一种情况。
一般绑定mousemove 、resize事件经常会导致
<script>
let num = 1;
let content = document.getElementById('content');
function count() {
content.innerHTML = num++;
};
content.onmousemove = count;
</script>
上述代码中,div 元素绑定了 mousemove 事件,当鼠标在 div区域中移动的时候会持续地去触发该事件导致频繁执行函数。录屏软件出错了就不放动图了,可以粘贴体验一下,这里放个图片大概感受一下
仅仅是移动了一下鼠标,就执行了那么多次,这肯定是不行的,所以,接下来让我们来看看防抖和节流是如何去解决这个问题的。
防抖函数分为非立即执行版和立即执行版。
非立即执行版:
function debounce(func, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args)
}, wait);
}
}
content.onmousemove = debounce(count,1000);
非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
我们依旧使用上述绑定 mousemove 事件的例子,通过上面的防抖函数,效果就明显变了
可以看到,在触发事件后函数 1 秒后才执行,而如果我在触发事件后的 1 秒内又触发了事件,则会重新计算函数执行时间。
上述防抖函数的代码还需要注意的是 this 和 参数的传递
let context = this;
let args = arguments;
防抖函数的代码使用这两行代码来获取 this 和 参数,是为了让 debounce 函数最终返回的函数 this 指向不变以及依旧能接受到 e 参数。
立即执行版:
function debounce(func,wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
let callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
}
content.onmousemove = throttle(count,1000);
立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。
因为不能录播看不出对比就不粘图了,下面一个也是
在开发过程中,我们需要根据不同的场景来决定我们需要使用哪一个版本的防抖函数,一般来讲上述的防抖函数都能满足大部分的场景需求。但我们也可以将非立即执行版和立即执行版的防抖函数结合起来,实现最终的双剑合璧版的防抖函数。
双剑合璧版:
/*
debounce:函数防抖
@params
func:要执行的函数
wait:间隔等待时间
immediate:在开始边界还是结束边界触发执行(true=>开始边界)
@return
可被调用的函数
- */
function debounce(func,wait,immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
let callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
else {
timeout = setTimeout(() => {
func.apply(context, args)
}, wait);
}
}
}
content.onmousemove = throttle(count,1000);
代码节流(throttle)
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。 节流会稀释函数的执行频率。
对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。
时间戳版:
function throttle(func, wait) {
var previous = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
content.onmousemove = throttle(count,1000);
在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次。
啊!不能放动图好难啊 ,建议自己真的要粘到代码运行看一下区别,我就随便放个图意思一下了。
定时器版:
function throttle(func, wait) {
let timeout;
return function() {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
content.onmousemove = throttle(count,1000);
在持续触发事件的过程中,函数不会立即执行,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次。
我们应该可以很容易的发现,其实时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候。
同样地,我们也可以将时间戳版和定时器版的节流函数结合起来,实现双剑合璧版的节流函数。
双剑合璧版:
/**
- @desc 函数节流
- @param func 函数
- @param wait 延迟执行毫秒数
- @param type 1 表时间戳版,2 表定时器版
*/
function throttle(func, wait ,type) {
if(type===1){
let previous = 0;
}else if(type===2){
let timeout;
}
return function() {
let context = this;
let args = arguments;
if(type===1){
let now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}else if(type===2){
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
}
实际应用的时候还是要看情况呀,这里就大概总结这么多啦。