防抖函数及其使用示例

285 阅读3分钟

一、防抖函数的使用场景及类型

  1. 使用场景:用于高频触发且有一定停顿的情况,单位时间内事件触发则等待时间会被重置,如:用户在短时间内多次点击登陆、搜索框根据输入的一部分值进行联想搜索(也可以使用节流)、短信验证码、resize等。
  2. 类型:防抖函数分为立即执行和非立即执行两种,立即执行的为前缘防抖,非立即执行的为延迟防抖。

二、防抖函数

  1. 前缘防抖

下面给了两种实现方式

使用定时器实现:

// 前缘防抖(定时器版本),在一定时间间隔内的连续触发只执行首次
function debounceImmediateExecution(fn, delay){
    let timer = null;
    return function (){
        let args = [...arguments];

        if(!timer){ // 首次触发或间隔 delay 时间后触发,立即执行 fn
            fn.apply(this, args);
            // 设置定时器
            timer = setTimeout(function (){
                timer = null;
            }, delay);
        }else{ // 在间隔时间内触发
            // 取消旧的定时器
            clearTimeout(timer);
            // 设置新的延时定时器
            timer = setTimeout(function (){
                fn.apply(this, args);
                timer = null;
            }, delay)
        }
    }
}

使用时间戳实现:

// 前缘防抖(时间戳版本,比使用定时器开销更低),在一定时间间隔内的连续触发只执行首次
function debounceImmediateExecutionPlus(fn, delay){
    let last = Date.now();
    let first = true; // 是否为首次执行
    return function (){
        let args = [...arguments];
        if(first){ // 首次触发
            fn.apply(this, args);
            last = Date.now();
            first = false;
        }else{ // 后续触发
            let now = Date.now();
            if(now - last >= delay){
                fn.apply(this, args);
            }
            last = now;
        }
    }
}
  1. 延迟防抖
// 延迟防抖,在一定时间间隔内的连续触发只执行最后一次
function debounceDelayExecution(fn, delay){
    let timer = null;
    return function (){ // 最后将这个闭包函数返回作为包装后的事件监听函数
        clearTimeout(timer); // 取消旧的定时器
        let _this = this; // this 指向监听的节点,此处不保存的话到了定时器回调函数中 this 就会变为 window
        let args = [...arguments]; // 事件监听函数的参数
        // 重置定时器
        timer = setTimeout(function (){
            fn.apply(_this, args);
        }, delay)
    }
}
// 个人理解:延迟防抖最好用定时器实现,因为需要在满足某个条件后,让 fn 在经过 delay 时间后执行
  1. 可选前缘或延迟防抖
// 防抖完整版(相当于前缘防抖和延迟防抖都使用定时器实现时的结合),可选前缘防抖(默认)或者延迟防抖
function debounce(fn, delay, isImmediate = true){
    let timer = null;
    return function (){
        let args = [...arguments];
        let _this = this; // 保存 this 供后续操作中使用

        if(timer){ // 已有定时器时,定时器需要重置,代表对中途连续触发的处理
            clearTimeout(timer); // 取消旧的定时器
            if(isImmediate){ // 使用前缘防抖时
                timer = setTimeout(function (){ // 创建新的定时器,用于时间延迟
                    timer = null; // 执行后置空
                }, delay);
            }else{ // 使用延迟防抖时
                timer = setTimeout(function (){ // 创建新的定时器
                    fn.apply(_this, args);
                    timer = null; // 执行后置空
                }, delay);
            }
        }else{ // 没有定时器时,代表对首次触发或者间隔时间>=delay时的触发进行处理
            if(isImmediate){ // 使用前缘防抖时
                fn.apply(this, args)
                timer = setTimeout(function (){ // 创建新的定时器,用于时间延迟
                    timer = null;
                }, delay);
            }else{ // 使用延迟防抖时
                timer = setTimeout(function (){ // 创建新的定时器
                    fn.apply(_this, args);
                    timer = null; // 执行后置空
                }, delay);
            }
        }
    }
}

三、完整使用示例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>防抖</title>
</head>
<body>

<div>
    <input type="button" value="点我次数加1" id = 'button'>
    <div>
        <span>
            当前点击次数为:
        </span>
        <span id = 'show'>
            0
        </span>
    </div>
</div>

<script>
    /**
     * 防抖函数
     */

    // 延迟防抖,在一定时间间隔内的连续触发只执行最后一次
    function debounceDelayExecution(fn, delay){
        let timer = null;
        return function (){ // 最后将这个闭包函数返回作为包装后的事件监听函数
            clearTimeout(timer); // 取消旧的定时器
            let _this = this; // this 指向监听的节点,此处不保存的话到了定时器回调函数中 this 就会变为 window
            let args = [...arguments]; // 事件监听函数的参数
            // 重置定时器
            timer = setTimeout(function (){
                fn.apply(_this, args);
            }, delay)
        }
    }
    // 个人理解:延迟防抖最好用定时器实现,因为需要在满足某个条件后,让 fn 在经过 delay 时间后执行


    // 前缘防抖(定时器版本),在一定时间间隔内的连续触发只执行首次
    function debounceImmediateExecution(fn, delay){
        let timer = null;
        return function (){
            let args = [...arguments];

            if(!timer){
                fn.apply(this, args);
                // 设置定时器
                timer = setTimeout(function (){
                    timer = null;
                }, delay);
            }else{
                // 取消旧的定时器
                clearTimeout(timer);
                // 设置新的延时定时器
                timer = setTimeout(function (){
                    timer = null;
                }, delay)
            }
        }
    }

    // 前缘防抖(时间戳版本,比使用定时器开销更低),在一定时间间隔内的连续触发只执行首次
    function debounceImmediateExecutionPlus(fn, delay){
        let last = Date.now();
        let first = true; // 是否为首次执行
        return function (){
            let args = [...arguments];
            if(first){ // 首次触发
                fn.apply(this, args);
                last = Date.now();
                first = false;
            }else{ // 后续触发
                let now = Date.now();
                if(now - last >= delay){
                    fn.apply(this, args);
                }
                last = now;
            }
        }
    }

    // 防抖完整版(相当于前缘防抖和延迟防抖都使用定时器实现时的结合),可选前缘防抖(默认)或者延迟防抖
    function debounce(fn, delay, isImmediate = true){
        let timer = null;
        return function (){
            let args = [...arguments];
            let _this = this; // 保存 this 供后续操作中使用

            if(timer){ // 已有定时器时,定时器需要重置,代表对中途连续触发的处理
                clearTimeout(timer); // 取消旧的定时器
                if(isImmediate){ // 使用前缘防抖时
                    timer = setTimeout(function (){ // 创建新的定时器,用于时间延迟
                        timer = null; // 执行后置空
                    }, delay);
                }else{ // 使用延迟防抖时
                    timer = setTimeout(function (){ // 创建新的定时器
                        fn.apply(_this, args);
                        timer = null; // 执行后置空
                    }, delay);
                }
            }else{ // 没有定时器时,代表对首次触发或者间隔时间>=delay时的触发进行处理
                if(isImmediate){ // 使用前缘防抖时
                    fn.apply(this, args)
                    timer = setTimeout(function (){ // 创建新的定时器,用于时间延迟
                        timer = null;
                    }, delay);
                }else{ // 使用延迟防抖时
                    timer = setTimeout(function (){ // 创建新的定时器
                        fn.apply(_this, args);
                        timer = null; // 执行后置空
                    }, delay);
                }
            }
        }
    }


    /**
     * 下面是过程的处理,节点获取,注册事件监听,函数封装等
     */
    let times = 0; // 计数
    let button  = document.getElementById('button');
    let show = document.getElementById('show');

    // 实际(最初)的点击事件处理函数,可以在这里进行网络请求之类的操作
    function handleClick(){
        ++times;
        // 这里是替换显示的文本,使用 textContent 比 innerHTML 更安全
        show.textContent = times;
    }

    // 将最初的事件处理函数进行包装,把 handleClick 进行防抖处理,返回闭包函数作为新的事件处理函数
    // let packingHandleClick = debounceDelayExecution(handleClick, 2000);
    // let packingHandleClick = debounceImmediateExecution(handleClick, 2000);
    // let packingHandleClick = debounceImmediateExecutionPlus(handleClick, 2000);
    let packingHandleClick = debounce(handleClick, 2000);

    // 注册事件监听器
    button.addEventListener("click", packingHandleClick);
</script>
</body>
</html>

想了解节流的朋友们可以看看我的另一篇文章:节流函数及其使用示例