这次一把搞明白防抖

170 阅读3分钟

防抖

当你一直触发click、input、resize等事件的时候,对应的函数不会立即调用执行,等你停下来的时候,并等待3秒(假如是3秒,具体等待时间可以作为参数传入)然后才执行对应的函数,假如3秒内你又触发,那继续等3秒

总而言之,言而总之,只有在某个时间内,没有再次触发这个事件时,才真正的调用相应的函数 image.png

应用场景:

  • 输入框输入搜索内容
  • 频繁点击某个按钮(比如你要点一个按钮删除某条数据,后台需要一定时间处理,你一直点,一直调删除接口,可能就会接口报错)
  • 缩放浏览器的resize事件
  • 监听浏览器scroll滚动事件,完成某些特定操作;

下面上代码:

基础防抖

不加防抖是这样

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <input type="text" />
        <script>
            const inputEl = document.querySelector("input");
            inputEl.oninput = function () {
                console.log("数你触发几次,看我打印多少次");
            };
        </script>
    </body>
</html>

image.png

实现一个基本的防抖

//debounce.js
    
function customeDebounce(fn, delay) {
    //记录一个定时器
    let timer = null
    const debounceFn = () => {
        //如果在delay时间内再次触发,就取消上一次的timer,否则会触发多次,因为
        //setTimeout会在delay时间到了把它的第一个回调参数放到宏任务对列,js线程会依次执行宏任务队列里面的这些回调函数
        //所以在delay时间内触发多次,每次就要在delay时间到之前取消上一次的timer,只留下最后一个
        if (timer) clearTimeout(timer)
        //重新计时,赋值一个新的timer
        timer = setTimeout(() => {
            fn()
            //timer变量存在闭包内,要手动销毁它
            timer = null
        }, delay)
    }
    //返回给调用者真正执行的函数
    return debounceFn
}
// debounce.html
 
    <body>
        <input type="text" />
        <script src="./debounce.js"></script>
        <script>
            const inputEl = document.querySelector("input");
            inputEl.oninput = customeDebounce(function () {
                console.log("数你触发几次,看我打印多少次");
            }, 2000);
        </script>
    </body>

image.png

加入参数

function customeDebounce(fn, delay) {
    let timer = null
    const debounceFn = function (...args) {
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, args)
            timer = null
        }, delay)
    }
    return debounceFn
}

本来fn是调用者直接触发的函数,现在把debounceFn返给调用者,那么就要让fn的this和debounceFn保持一致,那么debounceFn就不能再用箭头函数了,箭头函数不绑定this,由上层作用域决定,所以setTimeout中回调函数的this由上层作用域决定,就是debounceFn的this.

关于this绑定问题,可以查看 搞清楚this的几种绑定规则,将this指向一网打尽

    <body>
        <input type="text" />
        <script src="./debounce.js"></script>
        <script>
            const inputEl = document.querySelector("input");
            inputEl.oninput = customeDebounce(function (e) {
                console.log("数你触发几次,看我打印多少次");
                console.log(this,e);
            }, 2000);
        </script>
    </body>

image.png

取消功能

function customeDebounce(fn, delay) {
    let timer = null
    const debounceFn = function (...args) {
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, args)
            timer = null
        }, delay)
    }
    //函数作为对象,可以添加一个函数属性
    debounceFn.cancelDebounce = () => {
        //如果定时器还存在,就取消它
        timer && clearTimeout(timer)
    }
    return debounceFn
}
    <body>
        <input type="text" />
        <button>取消</button>
        <script src="./debounce.js"></script>
        <script>
            const inputEl = document.querySelector("input");
            const btnEl = document.querySelector("button");

            inputEl.addEventListener("input", function () {
                console.log("没加防抖,看我打印几次");
            });

            const debounceFn = customeDebounce(function (e) {
                console.log("加了防抖,看我打印几次");
                console.log(this, e);
            }, 1000);

            inputEl.addEventListener("input", debounceFn);

            btnEl.onclick = function () {
                console.log("点取消");
                debounceFn.cancelDebounce();
            };
        </script>
    </body>

image.png

立即执行

function customeDebounce(fn, delay, immediate = false) {
    let timer = null
    //定义一个变量控制第一次执行
    let isExecuted = false
    const debounceFn = function (...args) {
        if (timer) clearTimeout(timer)
        //第一次触发,进入条件内
        if (immediate && !isExecuted) {
            //立即执行一次
            fn.apply(this, args)
            //执行完改为true,意味着在delay时间内多次触发,只有第一次触发进入if条件,后面的用定时器延迟
            isExecuted = true
            //执行完直接返回,不需要setTimeout把第一次触发再延迟执行一次,后面的触发照旧进入setTimeout
            return
        }
        timer = setTimeout(() => {
            fn.apply(this, args)
            timer = null
            //执行完还原为false,delay时间过后再触发事件,上面的if立即执行重新生效
            isExecuted = false
        }, delay)
    }
    debounceFn.cancelDebounce = () => {
        timer && clearTimeout(timer)
        timer = null
        //还原false,既然取消那下一次触发就该回复if条件立即执行
        isExecuted = false
    }
    return debounceFn
}
    <body>
        <input type="text" />
        <button>取消</button>
        <script src="./debounce.js"></script>
        <script>
            let counter = 1;
            const inputEl = document.querySelector("input");
            const btnEl = document.querySelector("button");

            inputEl.addEventListener("input", function () {
                console.log(`没加防抖,看我执行${counter++}次`);
            });

            const debounceFn = customeDebounce(
                function () {
                    counter--;
                    console.log(`加了防抖,看我在第${counter++}次执行`);
                },
                1000,
                true
            );

            inputEl.addEventListener("input", debounceFn);

            btnEl.onclick = function () {
                console.log("点取消");
                debounceFn.cancelDebounce();
            };
        </script>
    </body>

image.png

返回值

function customeDebounce(fn, delay, immediate = false, callBack) {
    let timer = null
    let isExecuted = false
    const debounceFn = function (...args) {
        return new Promise((resolve, reject) => {
            try {
                if (timer) clearTimeout(timer)
                let res = undefined
                if (immediate && !isExecuted) {
                    res = fn.apply(this, args)
                    callBack && callBack(res)
                    resolve(res)
                    isExecuted = true
                    return
                }
                timer = setTimeout(() => {
                    fn.apply(this, args)
                    timer = null
                    callBack && callBack(res)
                    resolve(res)
                    isExecuted = false
                }, delay)
            } catch (error) {
                reject(error)
            }
        })
    }
    debounceFn.cancelDebounce = () => {
        timer && clearTimeout(timer)
        timer = null
        isExecuted = false
    }
    return debounceFn
}

setTimeout中是延迟执行,不能通过同步的方式来获取fn的返回值,可以通过两种方式来获取返回值,一是通过回调函数的方式在fn执行完后,把结果放到回调函数的参数中,另一种就是返回promise,调用者可以任意通过一种方式获取返回值

    <body>
        <input type="text" />
        <button>取消</button>
        <script src="./debounce.js"></script>
        <script>
            const debounceFn = customeDebounce(
                function () {
                    return "我是结果,看你怎么拿我";
                },
                1000,
                true,
                function (res) {
                    console.log(res); //我是结果,看你怎么拿我
                }
            );
            debounceFn().then((res) => {
                console.log(res); //我是结果,看你怎么拿我
            });
        </script>
    </body>

如果调用者参数传错,也可以捕获错误

    <body>
        <input type="text" />
        <button>取消</button>
        <script src="./debounce.js"></script>
        <script>
            const debounceFn = customeDebounce(
                function () {
                    return "我是结果,看你怎么拿我";
                },
                1000,
                true,
                "传的不是函数"
            );
            debounceFn()
                .then((res) => {
                    console.log(res);
                })
                .catch((err) => {
                    console.log(err); //TypeError: callBack is not a function
                });
        </script>
    </body>