JavaScript防抖原理与实现(debounce)

342 阅读5分钟

什么是函数防抖

概念:函数防抖(debounce),就是指触发事件后,在 n 秒内函数只能执行一次,如果触发事件后在 n 秒内又触发了事件,则会重新计算函数延执行时间。

1、为什么需要函数防抖?

前端开发过程中,有一些事件,常见的例如,onresize,scroll,mousemove ,mousehover 等,会被频繁触发(短时间内多次触发),不做限制的话,有可能一秒之内执行几十次、几百次,如果在这些函数内部执行了其他函数,尤其是执行了操作 DOM 的函数(浏览器操作 DOM 是很耗费性能的),那不仅会浪费计算机资源,还会降低程序运行速度,甚至造成浏览器卡死、崩溃。

2、函数防抖的实现

实现原理

函数防抖的要点,是需要一个 setTimeout 来辅助实现,延迟运行需要执行的代码。如果方法多次触发,则把上次记录的延迟执行代码用 clearTimeout 清掉,重新开始计时。若计时期间事件没有被重新触发,等延迟时间计时完毕,则执行目标代码。

代码实现

//HTML部分 
<div> 账户:<input type="text" id="myinput"> </div>
//JS部分 
function debounce(fun, wait=1500){
    //ES6语法 wait=1500 设置参数默认值,如果没有输入wait就会使用1500         
    let timeout = null; //闭包的知识,内层函数可以访问到这个timeout         
    return function(){
        if(timeout){
           clearTimeout(timeout) //如果存在定时器就清空
        } 
        timeout = setTimeout(function(){fun()},wait);       
    } 
}

function testUname(){console.log("输入结束!");} 
 
document.getElementById('myinput').addEventListener('input',debounce(testUname,1000));

上面的代码就是防抖函数的简单运用,只要你每次输入间隔大于一秒,那么永远不会打“印输入结束!”,直到你停止输入吗,这是因为每一次的输入都会清除上一次的计时器。

优化版:this指向和arguments修复

无论是防抖还是节流,我们都要解决两个问题,this指向和arguments。 如果没有特殊指向,setInterval和setTimeout的回调函数中this的指向都是window。这是因为JS的定时器方法是定义在window下的。

从下面这个代码中可以看出,setTimeout中的this指向的是window,这显然不是我们希望的,因为我们监听的是input输入框,所以我们希望定时器里面的this指向input。

//JS部分 
function debounce(fun, wait=1500){         
    let timeout = null;         
    return function(){             
        console.log(this); //<input id="myinput" type="text"> 
        console.log(arguments);//Arguments { 0: input, … }
   if(timeout){
       //如果存在定时器就清空
       clearTimeout(timeout); 
   }             
   timeout=setTimeout(function(){                 
       console.log(this);//Window    
       console.log(arguments);//Arguments { … }                 
       fun();
       },wait)}
} 

function testUname(){     
console.log("输入结束!"); 
} 

document.getElementById('myinput').addEventListener('input',debounce(testUname,1000))

那么有什么方法可以改变this指向吗?

1、一种简单的办法就是我们可以用参数把定时器外层函数的this和arguments保存下来。然后再通过apply改变定时器要执行的函数fun的指向。

//JS部分 
function debounce(fun,wait=1500){             
    let timeout = null;             
    return function(){                 
        let _this = this;               
        let arg = arguments;                 
        if(timeout){
            //如果存在定时器就清空                     
            clearTimeout(timeout)                 
        } 
        timeout = setTimeout(function(){                     
        console.log(_this);  //<input id="myinput" type="text"> 
        console.log(arg);   //Arguments { 0: input, … }                     
        fun.apply(_this,arg); //为函数指定this并执行
        },wait)
   } 
}

2、用ES6的箭头函数新特性: 箭头函数的 this 始终指向函数定义时的 this,而非执行时。箭头函数需要记着这句话:“箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined”。 所以也可以这样写:

//JS部分 
function debounce(fun,wait=1500){             
    let timeout = null;             
    return function(){                 
        if(timeout){
        //如果存在定时器就清空                     
        clearTimeout(timeout); 
        }                 
        timeout=setTimeout(()=>{fun.apply(this,arguments)},wait);
    } 
}

“立即执行版” 和 “非立即执行版”

函数防抖其实是分为。“立即执行版” 和 “非立即执行版” 的,根据字面意思就可以发现他们的差别,所谓立即执行版就是 

触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

 而 “非立即执行版” 指的是 触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。

这样,在限制函数频繁执行的同时,可以减少用户等待反馈的时间,提升用户体验。

在原来基础上,添加一个是否立即执行的功能。

\** 
 * @desc 函数防抖---“立即执行版本” 和 “非立即执行版本” 的组合版本 
 * @param func 需要执行的函数 
 * @param wait 延迟执行时间(毫秒) 
 * @param immediate---true 表立即执行,false 表非立即执行 
**/ 
function debounce(func,wait,immediate) { 
    let timer; 
    return function () { 
        let context = this; 
        let args = arguments; 
        if (timer) clearTimeout(timer);
        //存在定时器就清空 
        if (immediate) { 
            //立即执行 
            timer = setTimeout(function(){ func.apply(context, args) }, wait); 
        } else { 
            //非立即执行 
            var callNow = !timer; //第一次timer为undefine,callNow=true 
            timer = setTimeout(() => { 
            //间歌时间后 重置timer为空
            timer = null;}, 
            wait)
        }
        if (callNow) func.apply(context, args); 
   } 
}

3、函数防抖的使用场景

函数防抖一般用在什么情况之下呢?一般用在,连续的事件只需触发一次回调的场合。具体有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求;
  • 用户名、手机号、邮箱输入验证;
  • 浏览器窗口大小改变后,只需窗口调整完后,再执行 resize 事件中的代码,防止重复渲染。