1. 背景
现在的不少项目中都存在类似这样的业务:页面监听鼠标滚轮事件做出对应操作、搜索输入框输入文字时下面弹出联想结果弹框(类似百度输入文字效果)等等,一般情况下会直接绑定scroll事件监听、input事件监听,像用户每次操作页面滚动时,scroll事件触发的频率是很高的,如果在这些函数内部执行了其他函数,尤其是执行了操作DOM的函数,那不仅会造成计算机资源的浪费,还会降低程序运行速度,造成一些体验问题;或者在输入框频繁输入时,频繁调用后台接口,给后台造成太大压力,节流防抖的目的就是在尽量不影响用户体验的情况下,减少监听处理函数的调用次数,提升性能
2. 防抖
2.1 概念
函数防抖指的是,当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时
"函数防抖"的关键之处在于,在一个动作发生一定时间之后,才会执行特定的事件
2.2 简单的实现方案
举一个简单的例子,假设我们要实现鼠标在一个元素上面触发mousemove后鼠标停止移动时,将初始值为0的数字加1,且将加完后的结果显示在div上
<html lang="en">
<style lang="cn">
#content {
width: 200px;
height: 200px;
background: red;
}
</style>
<body>
<div id="content"></div>
<script>
let num = 1;
let oDiv = document.getElementById('content');
let changeNum = function () {
oDiv.innerHTML = ++num;
};
// 防抖逻辑
let deBounce = (fn, delay) => {
let timer = null;
return function (...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn(...args);
timer = null;
}, delay)
}
}
oDiv.onmousemove = deBounce(changeNum, 500);
</script>
</body>
</html>
如果不加防抖处理的话,鼠标在div上面不断移动,是会持续不停的触发多次mouseover事件且执行多次回调函数的,加上了防抖之后,实现了鼠标在div上面持续移动都不会执行回调函数,只有当鼠标停下来时才会执行回调函数。
主要的思路就是每次mousemove事件触发时,如果与上次间隔不到delay指定的时间间隔,将会通过clearTimeout去把上一次触发的事件给清除掉,然后再重新设置一个定时器开始计时,直到delay指定的时间完了这段时间内都没有再触发mousemove,才会执行回调函数。
2.3 实际应用场景
例子1:监听窗口尺寸改变事件
$(window).on('resize', debounce(doResizeTimer, 200));
例子2:页面中某个模块,需要根据模块中的内容高度决定是否只展示局部并在底部添加“展开全部”按钮
场景如图所示
$(this.$refs.descCon).find('img').on(
'load',
debounce(() => {
this.initToggle();
}, 200)
);
当某个模块中展示的内容是后台返回的一段富文本,里面包含文本、换行符、多张图片,我们需要根据图片渲染完成后,再去计算模块的总高度来决定是否只展示部分内容,剩余的隐藏并且在底部添加"展开全部"按钮,这里需要监听容器里面的img是否加载完成监听load事件,此时就可以用到防抖。
因为这个模块中可能存在多张图片,所以load事件可能触发多次,每次都触发initToggle的话性价比不高,所以使用防抖,起到的作用是如果在200ms内一直有图片触发load事件说明整个模块没完全渲染完成,initToggle函数一直不会被触发,只有当最后一张图片加载完后触发load事件且后续已经没有其他图片再次触发load了,则在200ms之后就会去执行initToggle函数,最理想的状态下,initToggle只会执行一次
注意:防抖的原理是事件在短时间内一直触发的话,回调函数一直不会执行的,所以防抖不太适用于需要实时响应的逻辑,比如页面在滚动时滚动需要实时作出响应,这种场景可能不适用防抖
3. 节流
3.1 概念
函数节流(throttle):规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
3.2 简单的实现方案
还是举一个例子,页面上有一个按钮,我想要在一段时间内,点击N次,只想要执行1次
<html lang="en">
<body>
<div class="div1">
<button>防抖按钮</button>
</div>
<script>
let btn = document.getElementsByTagName('button')[0];
let fn = () => console.log('我被触发了!');
// 节流逻辑
let throttle = function (func, delay) {
let timer = null;
let args;
return function () {
let context = this;
args = arguments;
if (!timer) {
timer = setTimeout(function () {
func.apply(context, args);
timer = null;
}, delay);
}
}
}
btn.onclick = throttle(fn, 500);
</script>
</body>
</html>
实现思路是:当触发事件的时候,我们设置一个定时器,再次触发事件的时候,如果定时器存在,就不执行,直到delay时间后,定时器执行回调函数,并且清空定时器,这样就可以设置下个定时器。当第一次触发事件时,不会立即执行函数,而是在delay毫秒后才执行。而后再怎么频繁触发事件,也都是每delay时间才执行一次。
3.3 实际应用场景
例子1:滚动事件监听
模块中显示用户评测,当评测内容过长的时候且点击"展开按钮"显示全部内容时,底下的收起按钮需要做一个吸附在可视区底部的效果,此时需要做一个页面滚动事件监听的处理,当滚动到评测区域快要离开可视区时,取消吸附效果
$(document).on(
'scroll',
throttle(scrollHandler, 200)
);
例子2:搜索输入框输入内容后下面弹出智能匹配内容
let throttleAjax = throttle(ajax, 1000);
input.addEventListener('keyup', function(e) {
throttleAjax(e.target.value)
})
4. 相关JS库
项目中需要使用到防抖、节流的操作时,不需要自己造轮子,网上已经有很多现成的方案,类似lodash这样的库已经提供了debounce和throttle方法,如果项目中刚好有这个依赖,可以直接从里面拿出来用。
5. 总结
- 函数防抖和函数节流都是防止某一时间频繁触发,但是这两个的原理却不一样。
- 函数防抖是某个时间间隔内不断触发的话就一直不会执行,只有当停止触发才会执行,而函数节流是某个间隔时间内不管触发多少次只执行一次。
使用场景
防抖debounce适用于
- window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
节流throttle适用于
- 监听滚动事件,比如是否滑到底部自动加载更多、滚动时对可视区中的元素做特定操作
- 搜索输入框智能联想,用户在不断输入值时在输入多个字符后再发送请求给后台(也可以使用防抖,看具体产品需求)