最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 终于搞懂:防抖和节流

    正文概述 掘金(缪培康)   2021-01-07   545

    一、缓冲

    缓冲,这是一个非常普遍的概念,凡是使某种事物进行减慢或减弱变化过程都可以叫缓冲。网页交互中也需要缓冲,也就是 防抖(debounce)节流(throttle) —— 让高频事件的反应慢一点,虽然是老生常谈,但也不容忽视。

    • debounce:一个事件被触发后,立即设定一个小延时。如果在延时期间事件再次被触发,则销毁前一个延时并重新开始延时;否则,开始执行处理函数。简而言之,就是不断地重新设置计时器举例:电梯门,若干秒内无人进入,则自动关门。
    • throttle:在事件连续触发的过程中,每隔一段时间执行一次处理函数。举例:文章自动保存,在编辑过程中每隔几分钟自动保存一次。

    二、Debounce

    1. debounce基本实现

    • 需求:鼠标在页面上移动,当移动停止 2 秒后,提示“鼠标移动了”。
    • 实现:根据前面的定义,在鼠标移动时设置新的计时器即可:
    let timer = null;
    function setTimer(){
        timer && clearTimeout(timer);
        timer = setTimeout(function () {
        	console.log("鼠标移动了");
        }, 2000)
    }
    window.onmousemove = setTimer;
    

    2. 封装

    • 需求:由于以上代码使用频繁,需要封装成一个便捷的工具函数,参数为事件处理函数和等待时长。
    • 实现:实参不一定会严格按照形参来传,注意防御
    /**
     * debounce函数 V1.0
     * @param {function} fn 事件处理函数,默认为一个空函数
     * @param {number} delay 延时时间,单位为毫秒
     * @return {function} resetTimer 返回一个可以重新设定计时器的函数
     */
    function debounce(fn=function(){}, delay=0){
        let timer = null;
        function resetTimer() {
            if(timer) {clearTimeout(timer)}
            // 要保证参数delay是自然数
            const delayTime = isNaN(parseInt(delay)) ? 
                0 : Math.abs(parseInt(delay));
            timer = setTimeout(fn, delayTime);
        }
        return resetTimer;
    }
    
    function onMove() { console.log("鼠标移动了") }
    window.onmousemove = debounce(onMove, 2000);
    

    3. 执行环境:this

    在事件处理函数中经常需要访问this对象。 上面的debounce函数被用在了window.onmousemove上,所以onMove里的this肯定指的是window。现在我们把debounce用在别的对象上,看看this的指向会不会变化:

    // ...debounce函数 V1.0
    //假设有个ID为menu的元素
    const oBtn = document.getElementById("menu"); 
    function onBtnHover() {console.log(this)};
    oBtn.onmousemove = debounce(onBtnHover, 1000);
    // 结果:window对象
    

    出问题了,onBtnHover里的 this 并不是oBtn,问题在debounce函数的setTimeout里:

    setTimeout(fn, delayTime); //相当于
    function callback(){
        fn();
    }
    setTimeout(callback, delayTime);
    

    这里的 fn 并没有与任何对象绑定,所以它属于window对象,因此this指向window。

    4. 解决this指向问题

    在下面的代码中,debounce为oBtn.onmousemove绑定了事件处理函数resetTimer。根据ES5中this的性质,该函数运行时,它的执行环境必然是调用者:

    function debounce(fn=function(){}, delay=0){
        function resetTimer() {
           console.log("resetTimer执行环境:", this);
        }
        return resetTimer;
    }
    const oBtn = document.getElementById("menu");
    function onBtnHover() {console.log(this)};
    oBtn.onmousemove = debounce(onBtnHover, 1000);
    // 结果:执行环境是oBtn
    

    因此,应该在resetTimer函数里将 this 保存下来,然后在setTimeout中使用call、apply函数修改this指向。debounce函数可以这样修改:

    /**
     * debounce函数 V2.0
     * @param {function} fn 事件处理函数,默认为一个空函数
     * @param {number} delay 延时时间,单位为毫秒
     * @return {function} resetTimer 返回一个可以重新设定计时器的函数
     */
    function debounce(fn=function(){}, delay=0){
        let timer = null;
        function resetTimer() {
            const context = this;  /**** 修改:保存执行环境 ****/ 
            if(timer) {clearTimeout(timer)}
            const delayTime = isNaN(parseInt(delay)) ? 
                0 : Math.abs(parseInt(delay));
            timer = setTimeout(
                function(){fn.apply(context)},  /**** 修改:应用执行环境 ****/ 
                delayTime
            );
        }
        return resetTimer;
    }
    
    const oBtn = document.getElementById("menu");
    function onBtnHover() {console.log("鼠标移动了,执行环境是:", this)};
    oBtn.onmousemove = debounce(onBtnHover, 1000);
    // 结果:执行环境是 oBtn
    

    5. 参数传递

    上面的代码保证了this指向正确,但是一旦试图为onBtnHover传参,就会影响到this指向。

    • 需求:onBtnHover函数打印的是为其传递的字符串
    • 实现:用一个匿名函数调用onBtnHover并传参
    // ...debounce函数 V2.0
    const oBtn = document.getElementById("menu");
    function onBtnHover(text) {console.log(text, this)};
    oBtn.onmousemove = debounce(
        function(){onBtnHover("鼠标移动了,执行环境是:")}, 
        1000
    );
    // 结果:执行环境是 window
    
    • 原因: 为了传参,我们用一个匿名函数来调用onBtnHover,因此apply(context)只是修改了这个匿名函数的this,而onBtnHover的this仍然未被修改。
    • 证明:在这个匿名函数内打印一下this
    // ...debounce函数 V2.0
    const oBtn = document.getElementById("menu");
    function onBtnHover(text) {console.log(text, this)};
    oBtn.onmousemove = debounce(
        function(){
            console.log("匿名函数执行环境:", this); /*** 修改 ***/
            onBtnHover("鼠标移动了,执行环境是:");
        }, 
        1000
    );
    // 匿名函数执行环境:oBtn
    // 鼠标移动了,执行环境是:window
    
    • 解决方法1: 在匿名函数里使用call、apply修改onBtnHover的this,并传参
    // ...debounce函数 V2.0
    const oBtn = document.getElementById("menu");
    function onBtnHover(text) {console.log(text, this)};
    oBtn.onmousemove = debounce(
        function(){
            console.log("匿名函数执行环境:", this); 
            onBtnHover.call(this, "鼠标移动了,执行环境是:"); /*** 修改 ***/
        }, 
        1000
    );
    // 匿名函数执行环境:oBtn
    // 鼠标移动了,执行环境是:oBtn
    

    **注意:**以上代码中如果使用apply,则参数必须以数组的形式传递:onBtnHover.apply(this, ["鼠标移动了,执行环境是:"])

    • 解决方法2:直接将onBtnHover定义为oBtn的一个方法
    // ...debounce函数 V2.0
    const oBtn = document.getElementById("menu");
    oBtn.onBtnHover = function(text) {console.log(text, this)};  /*** 修改 ***/
    oBtn.onmousemove = debounce(
        function(){
            console.log("匿名函数执行环境:", this);
            oBtn.onBtnHover("鼠标移动了,执行环境是:"); /*** 修改 ***/
        }, 
        1000
    );
    // 匿名函数执行环境:oBtn指向的元素
    // 鼠标移动了,执行环境是:oBtn指向的元素
    

    6. Debounce:总结

    Debounce是在绑定事件处理函数时使用的,它会为事件绑定一个不断重新设定计时器的函数。使用上面的debounce函数 V2.0 时需要向其传递真正的事件处理函数以及延时时间。 如果需要给事件处理函数传参,则要用上述的两种方式来控制this指向。

    三、Throttle

    节流与防抖的差别仅仅是控制方式不同,所以我们可以借鉴debounce的实现思路。

    1. throttle基本实现

    • 需求:在鼠标移动的过程中,每隔1秒打印一次“鼠标在移动”
    • 实现:鼠标刚移动就会设定一个计时器,同时设定一个标记,表明在计时期间若鼠标再次移动,则什么都不做,计时结束后执行处理函数,同时清除标记。这样一来,当再次移动鼠标时,新的计时器和标记就会被设定……
    /**
     * throttle函数 V1.0
     * @param {function} fn 事件处理函数,默认为一个空函数
     * @param {number} delay 延时时间,单位为毫秒
     * @return {function} setNewTimer 返回一个可以按时间间隔设定计时器的函数
     */
    function throttle(fn = function(){}, delay = 0){
        let isTiming = false; // 是否正在计时
        let timer = null;
        function setNewTimer(){
            if(isTiming) {return};
            
            //开始设置计时器和标记
            isTiming = true; // 设定标记,阻止下次触发
            const delayTime = isNaN(parseInt(delay)) ? 
                0 : Math.abs(parseInt(delay));
            let context = this;
            if(timer){clearTimeout(timer)}; //清除已有计时器
            timer = setTimeout(function(){
                fn.apply(context);
                isTiming = false;
            }, delayTime);
        }
        return setNewTimer;
    }
    
    function onMove(){console.log( "鼠标在移动" )};
    window.onmousemove = throttle(onMove, 1000);
    

    2. 传参与this指向

    • 需求:鼠标在指定的元素上移动时每隔1秒打印一次“鼠标在移动”,并且保证this指向该元素。
    • 实现:使用前面提到的两种方式即可
    • 方案1:在匿名函数里使用call、apply修改onBtnHover的this,并传参
    // throttle函数 V1.0
    const oBtn = document.getElementById("menu");
    function onBtnHover(text){console.log( text, this )};
    oBtn.onmousemove = throttle(
        function(){
            console.log("匿名函数执行环境:", this);
            onBtnHover.call(this, "鼠标在移动,执行环境:")
    }, 1000);
    
    • 方案2:直接将onBtnHover定义为oBtn的一个方法
    // throttle函数 V1.0
    const oBtn = document.getElementById("menu");
    oBtn.onBtnHover = function(text){console.log( text, this )};
    oBtn.onmousemove = throttle(
        function(){
            console.log("匿名函数执行环境:", this);
            oBtn.onBtnHover("鼠标在移动,执行环境:")
    }, 1000);
    

    四、总结

    至此,debounce和throttle均已实现完毕,我们还可以向它们添加更多参数来扩展其功能,比如用另外一个参数表示是否需要立即执行,等等。


    起源地下载网 » 终于搞懂:防抖和节流

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元