啥是节流?
节流是保证在一段时间内,代码只执行了一次。这个一段时间内指的是不管用户操作了几次,最终仅执行一次。比如说一个按钮,用户狂点按钮,但是如果用节流技术的话,不管用户点击了几次,最终某个时间段内只执行了一次代码。这个时间段是可以自行设置,比如说每一秒执行一次。
啥是防抖?
防抖其实和节流有些类似,毕竟它们的最终目的都是如出一辙。防抖是在一段时间结束之后,才触发一次事件。如果一段时间内为结束再次触发了事件,那么就会重新计算这段时间。同样的例子,还是用户狂点按钮,但是仅在用户停止点击按钮后的一段时间之后才会执行一次。如果用户暂停点击按钮的时间不到一段时间内又再次点击按钮,那么就会重新计算时间。这个时间同样可以自行设置。
为啥要防抖或节流呢?
为了优化高频率事件,比如说 onscroll 滚动 oninput 搜索框联想 resize 窗口大小变化 onkeydown onkeyup… 等等。这些高频率事件很有可能导致页面卡顿,影响用户体验。运用防抖和节流可以有效降低代码的执行频率,从而解决高频率事件的页面卡顿问题。或许还有疑问,为啥高频事件就会导致页面卡顿呢?这就要从页面的展示过程说起了。
页面的展示过程
展示过程大致为以下顺序:
Javascript -> Style -> Layout -> Paint -> Composite
首先,Javascript 阶段会往页面中添加一些 DOM 或动画,然后到 Style 阶段确定每个 DOM 应该用什么样式规则。在 Layout 阶段布局,最终确定 DOM 显示的位置和大小。在 Paint 阶段进行 DOM 的绘制,它是在不同层上进行绘制。注意,样式变化是重绘,布局和位置变化是重排。重排一定导致重绘,重绘不一定导致重排。最后一个阶段 Composite 进行渲染层合并。(所以做一些动画效果尽量用 CSS3 的 transform 等属性,因为该属性是脱离文档流,不用合并渲染层的。) 由此可见,如果触发了很多高频率的事件,就会导致页面不停的确定位置和大小,不停的重排重绘并且合并渲染层。所以导致页面卡顿也可以解释了。
接下来会用例子来一步步实现节流和防抖的原理。
节流
首先 比如页面上有个按钮,用户可以点击该按钮。该按钮上绑定了一个点击事件,用户可以疯狂点击触发该事件,肯定结果就是疯狂触发该事件。目标是让该按钮不管用户点击的多快,最终该事件每秒仅执行一次。
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<meta http-equiv=”X-UA-Compatible” content=”ie=edge”>
<title>throttle</title>
<style>
.btn{
width: 250px;
height: 60px;
background-color: hotpink;
color: #fff;
display: block;
text-align: center;
line-height: 60px;
cursor: pointer;
border-radius: 10px;
}
</style>
</head>
<body>
<btn class=”btn”> 按钮 </btn>
<script>
let btn = document.getElementsByClassName(‘btn’)[0];
function logger(){
console.log(‘log’);
}
/* 按钮绑定了一个事件 打印 log */
btn.addEventListener(‘click’,logger);
</script>
</body>
</html>
可以看到,用户疯狂点击了 20 次,那么该事件也理所当然的执行了 20 次,这显然不是我们想要的。基础版:
<body>
<btn class=”btn”> 按钮 </btn>
<script>
let btn = document.getElementsByClassName(‘btn’)[0];
function logger(){
console.log(‘log’);
}
btn.addEventListener(‘click’,throttle(logger,1000));
function throttle(func, wait){
/* 上次的时间戳 默认第一次 0 */
let pre = 0;
return function(){
let now = Date.now();
/* 如果当前时间与上次时间的间隔大于 wait */
if(now – pre > wait){
func.apply(this,arguments);
pre = now;
}
}
}
</script>
</body>
为了尽可能的减少篇幅,把一些无用的代码都删除了。定义一个 throttle 方法,该方法传入了两个参数,一个是要执行的事件,另一个是间隔时间。该 throttle 方法是一个闭包的写法,并且返回了一个函数。首先定义了上次的时间戳 pre,pre 默认第一次为 0。然后获取到当前时间,用当前时间减去上次的时间戳也就是 pre,如果这个差值大于了传递的时间间隔 wait,也就表明可以执行下一次的函数了。所以执行方法并且传递 this 和参数。并把当前时间赋给 pre,以便做下一次节流的判断。看下效果:
可以看到,虽然疯狂点击按钮,但是事件却没有疯狂触发,保持了每一秒执行一次的速度。也就达成了我们的目标。但是还有一个问题就是,我最后点击按钮的那次也应该触发事件,但是结果并没有。需要补上最后一次没有触发事件的问题,接下来优化它。进阶版:
<body>
<btn class=”btn”> 按钮 </btn>
<script>
let btn = document.getElementsByClassName(‘btn’)[0];
function logger(){
console.log(‘log’);
}
btn.addEventListener(‘click’,throttle(logger,1000,{trailing:true}));
function throttle(func, wait, options){
let pre = 0;
/* 定义一个 timeout 定时器 */
let timeout;
return function(){
let now = Date.now();
if(now – pre > wait){
if(timeout){
clearTimeout(timeout);
timeout = null;
}
func.apply(this,arguments);
pre = now;
}else if(!timeout && options.trailing !== false){
/* 如果当前时间和上次️时间的间隔小于 wait 并且 trailing 为 true */
timeout = setTimeout(later,wait-(now-pre));
}
}
function later(){
func.apply(this,arguments);
}
}
</script>
</body>
很明显看到,进阶版多传了一个参数对象,trailing:true。该参数用来表示是否执行最后一次触发的方法。在函数中,首先定义了一个空的定时器变量 timeout,用来计算时间间隔。其次多了一个 else if 的条件判断,判断如果时间间隔小于 wait,就表示该方法要保留起来延迟去执行。所以生成了一个定时器,延迟执行 later 函数,later 函数就是执行该 func 函数。此处注意一点,这个延迟时间的问题。延迟时间不能是 wait,必须是 wait 减去当前时间和上次时间的时间奖额。剩下的才是剩余时间延迟。还有一点要注意,在 if 中一定要清楚定时器,不然会影响 else if 的条件判断。经过测试,确实能在点击的最后一次后,延迟不到一秒触发了该事件。剩下最后一个优化点,其实第一次点击按钮,也应该延迟触发事件。目前的版本是点击按钮的第一次就直接触发该事件。优化它: