JS防抖与节流
本文已发布在:JS防抖与节流 -Aiysosis Blog(aiysosis.ink)
前言
防抖和节流是前端编程中常见而重要的需求,如在注册时需要根据用户的输入值判断用户名是否已被占用、实时监听浏览器的滚动事件、用户疯狂点击按钮等等。如果不对请求的频率加以限制,那么会造成资源浪费和页面的不流畅,防抖和节流正是为了应对这样的情况而产生的。
基础知识
在防抖节流的实现中我们主要利用了JavaScript的闭包特性来维持一个单例且可访问的定时器,因此我们首先需要了解闭包相关的概念。这里推荐下这个系列的文章,讲解得还是比较系统的(不是广告哦🙂)。
深入理解javascript原型和闭包(14)——从【自由变量】到【作用域链】 - 王福朋 - 博客园 (cnblogs.com)
深入理解javascript原型和闭包(15)——闭包 - 王福朋 - 博客园 (cnblogs.com)
同时,函数的封装,以及定时器的特性,可能会导致this
的指向错误,所以我们最好还要了解this
指向相关的知识。简单来说,函数的this
会指向调用它的对象,当我们在定时器中调用函数,由于浏览器的执行原理,this
会指向全局对象window
,这样就丢失了原有的指向性。
最后,我们还需要了解函数的arguments
,这是函数的内置属性,它是一个类数组
对象,记录了传入的参数,它的原型链上面没有数组的方法,但是我们可以这样操作:
function fn(){
let arr = Array.prototype.slice.apply(arguments,[0]);
//or arr = Array.prototype.slice.call(arguments,0)
console.log(arr);
}
fn(1,'2',[3,4]);//output:[ 1, '2', [ 3, 4 ] ]
这个知识点在防抖与节流中用不上,只是我比较喜欢发散就写下来了,权当拓展和巩固。但是这里的
apply
和call
方法还是需要提前了解的,它们的主要用途就是绑定函数执行时的this
。
情景
在这里我们结合具体的情景进行代码的编写。场景很简单,就是一个计数器和一个按钮,用户点击按钮,计数器加1,以此模拟请求的发送。
<div class="wrapper">
<div id="div">0</div>
<button id="btn" >click me</button>
</div>
//js
let div = document.getElementById('div');
let btn = document.getElementById('btn');
function add(a,b,c){//参数没有实际作用
console.log(this);
console.log('args: ',a,b,c);
let num = parseInt(div.innerHTML);
num ++;
div.innerHTML = ''+num;
}
btn.onclick = add(1,2,3);
当我们使用我们的麒麟臂疯狂点击按钮,可以看到数字在飞快地增长,这还只是一个用户发出的请求。因此我们需要通过代码把请求的频率控制在合理的范围内。
防抖
防抖的主要思想是:让用户的点击延迟一定时间(这里假设是1秒)生效,如果用户在一秒内又触发了点击事件,那就重新刷新等待的时间。只有当定时器正常走完,事件才被触发。简单说就是我知道你很急但是你先别急😅。
常见版本
function debounce(fn,wait){
let timer = null;
return function(){
let ctx = this;//绑定this
let args = arguments;//接受参数
if(timer)clearTimeout(timer);
timer = setTimeout(function(){
fn.apply(ctx,args);//利用apply调用函数
},wait)
}
}
这样做有一个缺点,如果用户非常急,从头到尾一直在点,那么在高频点击期间一次请求都不会发出,我们可以稍作修改,至少让用户的第一次点击生效,不然除了用户,产品和客服可能都会很急😂。
function debounceImm(fn,wait){
let timer = null;
return function(){
let _self = this;
let args = arguments;
clearTimeout(timer);
if(timer === null){
fn.apply(_self,args);
}
timer = setTimeout(function(){
timer = null;
},wait);
}
}
全是坑
当我兴高采烈地准备使用的时候,掉坑里了。如果add
没有参数,这两种写法都很完美,this指向了按钮,防抖功能也都能实现。
btn.onclick = debounce(add,1000);
btn.onclick = debounce(function(){...},1000);//里面是add的逻辑
但是当add有了参数,事情开始变得不对劲了。
btn.onclick = debounce(add(1,2,3),1000);//不行,add(1,2,3)立即执行了
btn.onclick = debounce(add,1000)(1,2,3);//同上,返回的函数被立即执行
btn.onclick = debounce(function(){add(1,2,3);},1000);//功能ok,但是this指向了window
btn.onclick = debounce(()=>add(1,2,3),1000);//同上,this指向了window
正解:
btn.onclick = debounce(function(){add.apply(this,[1,2,3])},1000);
这种调用方法实在是过于难受了,对于开发非常不友好。
改进
通过剩余参数和箭头函数,我们可以在简化代码的同时让调用方式更加友好。
function debounce(fn,wait,...args){
let timer = null;
return function(){
if(timer)clearTimeout(timer);
timer = setTimeout(()=>{
fn.apply(this,args);
},wait)
}
}
立即执行:
function throttleImm(fn,wait,...args){
let timer = null;
return function(){
if(timer === null){
fn.apply(this,args);
timer = setTimeout(()=>{
timer = null;
},wait)
}
}
}
调用:
//无参数
btn.onclick = debounce(add,1000);
btn.onclick = debounce(function(){/*...*/},1000);
//有参数
btn.onclick = debounce(add,1000,1,2,3);
btn.onclick = debounce(function(a,b,c){/*...*/},1000,1,2,3);
这样调用起来会方便很多,利用箭头函数简化了this的保存与再赋值,利用剩余参数统一了函数的调用方法。
节流
终于来到了节流部分,顺着刚才的情景,即使我们让用户的第一次操作生效,但如果用户很急,并且第一次操作又失败了,那么同样地,在疯狂点击的这段时间用户得不到任何响应,我们想要的是降低用户请求的频率,而不是让用户一次请求也发不出,因此节流应运而生。
节流的主要思想是:当用户疯狂点击时,可以以一个合适的频率不断发送请求(相当于为请求的频率设置了一个上限),我们同样可以基于定时器来实现。(这里的写法就直接应用箭头函数和剩余参数了)
function throttle(fn,...args){
let timer = null;
return function(){
if(timer === null){//如果已经有计时器那么就不用管了
timer = setTimeout(()=>{
fn.apply(this,args)
clearTimeout(timer);
timer = null;
},1000)
}
}
}
立即执行:
function throttleImm(fn,wait,...args){
let timer = null;
return function(){
if(timer === null){
fn.apply(this,args)//就是调换了一下顺序
timer = setTimeout(()=>{
timer = null;
},wait)
}
}
}
调用:
//无参数
btn.onclick = throttle(add,1000);
btn.onclick = throttle(function(){/*...*/},1000);
//有参数
btn.onclick = throttle(add,1000,1,2,3);
btn.onclick = throttle(function(a,b,c){/*...*/},1000,1,2,3);