如果事件处理函数无限制调用,会大大加重浏览器的工作量,有可能导致页面卡顿影响体验;后台接口的频繁调用,不仅会影响客户端体验,还会大大增加服务器的负担。
让某个函数在一定 事件间隔条件(去抖debounce) 或 时间间隔条件(节流throttle) 下才会去执行,避免快速多次执行函数(操作DOM,加载资源等等)给内存带来大量的消耗从而一定程度上降低性能问题。
针对这个问题,一般有两个方案: 防抖 (Debounce) 节流 (Throttle)
在事件被触发n秒后,再去执行回调函数。如果n秒内该事件被重新触发,则重新计时。结果就是将频繁触发的事件合并为一次,且在最后执行。
电梯5秒后会关门开始运作,如果有人进来,等待5秒,5秒之内又有人进来,5秒等待重新计时…直至超过5秒,电梯才开始运作。
scroll事件(资源的加载)
mousemove事件(拖拽)
resize事件(响应式布局样式)
keyup事件(输入框文字停止打字后才进行校验)
每当事件触发,就去重置定时器。直至最后一次事件被触发,n秒后再去执行回调函数。
先做基本的准备(篇幅原因,HTML部分省略):
let container = document.getElementById('container');
// 事件处理函数
function handle(e) {
console.log(Math.random());
}
// 添加滚动事件
container.addEventListener('scroll', handle);
我们发现,每滚动一下,控制台就会打印出一行随机数。
我们现在写一个最基础的防抖处理:
function debounce(func, wait) {
var timeout;//标记
return function() {
clearTimeout(timeout);
timeout = setTimeout(func, wait);
}
}
事件也做如下改写:
container.addEventListener('scroll', debounce(handle, 1000));
现在试一下, 我们会发现只有我们停止滚动1秒钟的时候,控制台才会打印出一行随机数。
以上基础版本会有两个问题,请看如下代码:
// 处理函数
function handle(e) {
console.log(this); //输出Window对象
console.log(e); //undefined
}
没错,当我们不使用防抖处理时,handle()函数的this指向调用此函数的container,而在外层使用防抖处理后,this的指向会变成Window。
其次,我们也要获取到事件对象event。
所以我们要对防抖函数做以下改写:
function debounce(fn, wait) {
let timeout;
return function() {
let that = this;
let arg = arguments;
clearTimeout(timeout);
timeout = setTimeout(function(){
fn.apply(that,arg)//使用apply改变this指向
}, wait);
}
}
复制代码当然了,如果使用箭头函数便可以省去外层声明。
function debounce (fn, delay) {
return args => {
clearTimeout(fn.id)
fn.id = setTimeout(() => {
fn.call(this, args)
}, delay)
}
}
以上的情况都是只有当连续触发停止后才执行,那如果我们想让事件第一次触发就执行,后面的连续触发都不执行,直到停止触发一段时间才可以再次触发(比如防止频繁点击),该如何处理呢?
那么可以利用同样的原理,稍作修改即可:
function debounce(fn, wait) {
let timeout;
return function(){
let arg = arguments;
let that = this;
clearTimeout(timeout);
!timeout && fn.apply(that,arg)
timeout = setTimeout(function(){
timeout = null;
}, wait);
}
}
规定一个时间n,n秒内,将触发的事件合并为一次并执行。
电梯等第一个人进来之后,5秒后准时运作,不等待,若5秒内还有人进来,也不重置。
function throttle(fn, wait) {
let timeout;
return function () {
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
fn.apply(this, arguments)
}, wait)
}
}
}
function throttle (fn, delay) {
return args => {
if (fn.id) return
fn.id = setTimeout(() => {
fn.call(this, args)
clearTimeout(fn.id)
fn.id = null
}, delay)
}
}
用滚动事件来描述节流,其实是一个非常典型的场景,比如需要用滚动事件判断是否加载更多等。
和防抖函数类似,以上的情况是先等待后触发,如果我们想让事件先触发后等待,该如何处理呢?网上大部分文章都告诉你用时间戳的方式去实现,其实只要像防抖一样稍作修改即可实现。
function throttle(fn, wait) {
let timeout;
return function () {
if (!timeout) {
fn.apply(this, arguments)
timeout = setTimeout(() => {
timeout = null;
}, wait)
}
}
}
复制代码这样,我们就会发现第一次触发函数就会立即生效。