js中实现防抖与节流

防抖节流是怎么一回事?

  • 防抖是防止一些监听函数触发得过于频繁而给服务器造成压力,例如搜索框在用户停止键入后达到2秒才触发一次补全请求。
  • 节流是停止一段时间后才允许再次被执行。例如获取验证码按钮设置相隔30秒才能按一次。
  • 防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

场景:输入框

有一个输入框,给其加上监听事件onkeyup,或者使用eInput.addEventListener(“keyup”, func)的形式。这个func的功能为:每触发一次,计数器加一,并更新视图中的触发次数中的数字。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>debounce</title>


    <style>
        .container {
            padding-top: 100px;
            padding-right: 30px;
            padding-left: 30px;
            margin-right: auto;
            margin-left: auto;
        }
    </style>

</head>

<body>
    <div class="container">
        <div>
            <input id="search01" type="text" />
            <div>触发次数:<span id="result01"></span></div>
        </div>
    </div>
	<script>
		let counter01 = 0;
		let eSearch01 = document.getElementById("search01");
		let eResult01 = document.getElementById("result01");

    </script>
</body>

</html>

防抖版本

版本一 一旦触发keyup就请求

思路

直接加个addEventListener就好了。

function onSearch(){
    console.log(this);
    console.log(event);
    eResult01.innerHTML = ++count1;
    // TODO: 发送异步请求
    // ....
}

eSearch1.addEventListener("keyup", onSearch1);

img

分析

此时,this的指向是没问题的,成功地指向了input搜索框,同时event的监听也是没问题的。

这样写会发生什么情况呢?显然,无论我们输入什么,每按下一次键盘,函数就会触发一次。

例如,我们输入的是中文拼音,那么就会疯狂触发这个onSearch函数,这种情况肯定是不行的。

如图,我打出“拼音”这两个字,总共按了键盘"pinyin" + 空格共7次,那么就会触发7次函数,这将给后端补全查询的服务器增加巨大压力。




版本二 使用防抖装饰器初体验

思路

用一个装饰器,包装这个onSearch函数,改成每触发一次keyup事件就执行这个函数,这个函数主要完成以下几个动作。

  • 定义一个timeout来记录定时器的状态

  • 返回一个function,根据timeout的状态来执行操作。

    • 如果timeout为true,表明此时宏队列table中注册有这个onSearch的定时任务,这时可以表示用户还处于输入的过程,在不断触发keyup事件。那么此时将这个注册了的onSearch从宏队列table中删除。
    • 如果timeout为fales,表明用户还未输入或超过一定时间没有触发keyup事件
    • 在保证timout为false的情况下,再次注册定时任务onSearch,这样只要用户输入的间隔未达到2000毫秒时,便会不断重新往宏队列table中注册定时任务onSearch,必须保证直到计时结束后,才执行一次onSearch函数。
// 节流装饰器, 默认等待2000毫秒
function debounce01(func, wait = 2000){
	let timeout;
    //console.log(this);
    //console.log(event);
    //console.log(arguments);
	return function(){
		if(timeout){
			clearTimeout(timeout)
		}
		timeout = setTimeout(func, wait);
	}
}

function onSearch02() {
    // this将会指向window
    console.log(this);

    // undefined
    console.log(event);
	
    // onSearch()...  __proto__ 等
    // console.log(arguments)
    
    eResult02.innerHTML = ++counter2;
    
    // TODO: 发送请求
}

eSearch02.addEventListener("keyup", debounce01(onSearch02, 1000));
分析

使用debounce装饰器来装饰onSearch后,的确做到了一个所谓的防抖效果。

但这样子就出现了bug:this指针指向了window,并且keyup的event对象也获取失败了。

img

经过分析,发现是setTimeout导致的this指针丢失,指到了window。

因为setTimeout会将任务注册到宏队列中,直到主程序中的同步任务和微队列中的任务执行完后才开始执行。setTimeout里的函数在setTimeout执行的时候,才开始计时,计时完成就进入任务队列,当执行栈执行完就开始执行任务队列。(即,清空同步队列和微队列后,setTimeout才开始计时,计时结束后才将目标函数注册入宏队列中)。

如下图,setTimeout中的function()执行时,此时function()的上下文变成了window对象,即这个obj.getNumLater()函数执行时,做的事情是在宏队列table中注册了这个function,然后函数就执行完了。

怎么改进呢?当然得在this.getNumLater()中捕获当前的this值,然后传入这个setTimeout中的function,以此防止丢失this指针,请看接下来的迭代版本。

img




版本三 能够保证this和event能正常捕捉的防抖

思路

大致逻辑和版本二差不多,但是要使用apply来保存this指向

// 防抖 语法糖2
function debounce2(func, wait = 1000) {
    let timeout=null;

    return function () {
        let that = this;
        let args = arguments;
        args.event = event;

        // 思考:只有这里能正确得拿到this arguments event 为什么?
        console.log("-------------------------------------");
        console.log(that);
        console.log(args);
        console.log(event);
        console.log(timeout);
        console.log("-------------------------------------");

        if (timeout) {
            clearTimeout(timeout);
        }
        timeout = setTimeout(function () {
            // 不这样写,onSearch就拿不到event事件,为什么?
            func.apply(that, args);    // 传递给func内部
            // func.apply(that);    // 传递给func内部
        }, wait);
    }
}


function onSearch3() {
    // input对象
    console.log(this);

    // undefined
    console.log(event);

    // Arguments [KeyboardEvent, callee: ƒ, Symbol(Symbol.iterator): ƒ]
    // 为什么这里才能拿到KeyboardEvent事件?
    console.log(arguments);

    // 更新视图
    eResult3.innerHTML = ++count3;

    // TODO: 发送ajax请求
}

eSearch3.addEventListener("keyup", debounce2(onSearch3, 1000));
分析

img




节流版本

版本一 节流之初体验

输入框中,开始输入后,立刻执行函数,然后等到停止触发n秒后,才可以重新触发执行,该如何实现?

思路
  • 增加一个immediate参数,为true就是节流模式,为false就是防抖模式,并用其来分支

  • 防抖模式下的思路和之前一样,使用一个timeout变量来控制一个setTimeout

  • 每次触发函数,都会将timeout置为空

  • 节流模式下,使用一个rightNow变量来控制setTimeout,且rightNow = !timeout

  • 每次输入触发函数,会新注册setTimeout任务,这个任务计时完成后,会将timout设置为null

function onSearch4() {
    // input对象
    console.log(this);

    // undefined
    console.log(event);

    eResult4.innerHTML = ++count4;
    // TODO: 发送ajax请求
}


function debounce3(func, wait = 1000, immediate = false) {
    let timeout;

    return function () {
        let that = this;
        let args = arguments;
        if (timeout) {
            clearTimeout(timeout);
        }

        // 不立刻执行, 和之前一样
        if (!immediate) {
            console.log("不立即执行");
            timeout = setTimeout(function () {
                func.apply(args);
            }, wait);
        } else {
            // 立即执行
            console.log("立即执行");

            let rightNow = !timeout; // 通过该参数控制只执行一次

            // 设置定时清理
            timeout = setTimeout(function () {
                timeout = null;
            }, wait);

            if (rightNow) {
                func.apply(that, args);
            }
        }
    }
}

eSearch4.addEventListener("keyup", debounce3(onSearch4, 1000, true))

分析

img




版本二 我希望它可以在需要时马上触发

debounce3必须等n秒后才能能重新触发,现在希望能够取消防抖,让它能够立刻恢复执行,怎么实现?

思路
  • 在上一个版本的基础上,设置debounce.cancel函数,这是为了能够和闭包函数debounce共享到timeout变量
  • debounce.cancel的功能为,一旦触发,就将timeout变量设置为null。好,既然之前设置,直到计时结束后,timeout才设置为null,那我提前将timeout设置为null,就能成功提前结束计时了。
function debounce4(func, wait = 1000, immediate = false) {
    let timeout;
    let debounce = function () {
        let that = this;
        let args = arguments;
        if (timeout) clearTimeout(timeout);

        // 立刻执行
        if (immediate) {
            let rightNow = !timeout; // 通过该参数控制只执行一次
            timeout = setTimeout(function () {
                timeout = null;
            }, wait);
            if (rightNow) {
                func.apply(that, args);
            }
        } else {
            timeout = setTimeout(function () {
                func.apply(that, args);
            }, wait);
        }
    };
    // 取消函数
    debounce.kkk = function () {
        console.log("这是cancel");
        clearTimeout(timeout);
        timeout = null;
    }
    return debounce;
}

let doSearch = debounce4(onSearch5, 15000, true)


eSearch5.addEventListener("keyup", doSearch);
eBtn1.addEventListener("click", doSearch.kkk)
分析

原本按下键盘触发第一次后,第二次触发keyup事件需要相隔15秒。

而按下“点击立即运行”后,则立马恢复为最初的状态。

img

完整代码

拿去细细品吧。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>debounce</title>


    <style>
        .container {
            padding-top: 100px;
            padding-right: 30px;
            padding-left: 30px;
            margin-right: auto;
            margin-left: auto;
        }
    </style>

</head>

<body>
    <div class="container">
        <div>
            <input id="search1" type="text" />
            <div id="result1"></div>

            <input id="search2" type="text" />
            <div id="result2"></div>

            <input id="search3" type="text" />
            <div id="result3"></div>

            <input id="search4" type="text" />
            <div id="result4"></div>

            <input id="search5" type="text" /><button id="btn1">点击立马执行</button>
            <div id="result5"></div>

            <script>
                // -----------------------------------------------------
                let count1 = 0;
                let eSearch1 = document.getElementById("search1");
                let eResult1 = document.getElementById("result1");

                function onSearch1() {
                    // this将会指向input对象
                    console.log(this);

                    // KeyboardEvent
                    console.log(event);

                    eResult1.innerHTML = ++count1;
                    // TODO: 发送ajax请求
                }

                eSearch1.addEventListener("keyup", onSearch1);


                // -----------------------------------------------------

                // 语法糖1 防抖
                function debounce1(func, wait = 1000) {
                    let timeout;
                    return function () {
                        if (timeout) {
                            clearTimeout(timeout);
                        }
                        timeout = setTimeout(func, wait);
                    }
                }

                let count2 = 0;
                let eSearch2 = document.getElementById("search2");
                let eResult2 = document.getElementById("result2");

                function onSearch2() {
                    // this将会指向window
                    console.log(this);

                    // undefined
                    console.log(event);

                    eResult2.innerHTML = ++count2;
                    // TODO: 发送ajax请求
                }

                eSearch2.addEventListener("keyup", debounce1(onSearch2, 1000));



                // -----------------------------------------------------
                let count3 = 0;
                let eSearch3 = document.getElementById("search3");
                let eResult3 = document.getElementById("result3");

                // 防抖 语法糖2
                function debounce2(func, wait = 1000) {
                    let timeout;
                    return function () {
                        let that = this;
                        let args = arguments;

                        if (timeout) {
                            clearTimeout(timeout);
                        }
                        timeout = setTimeout(function () {
                            func.apply(that, args);    // 传递给func内部
                        }, wait);
                    }
                }


                function onSearch3() {
                    // input对象
                    console.log(this);

                    // undefined
                    console.log(event);

                    eResult3.innerHTML = ++count3;
                    // TODO: 发送ajax请求
                }

                eSearch3.addEventListener("keyup", debounce2(onSearch3, 1000));


                // -----------------------------------------------------
                // 立刻执行函数,然后等到停止触发n秒后,才可以重新触发执行,该如何执行? 

                let count4 = 0;
                let eSearch4 = document.getElementById("search4");
                let eResult4 = document.getElementById("result4");

                function onSearch4() {
                    // input对象
                    console.log(this);

                    // undefined
                    console.log(event);

                    eResult4.innerHTML = ++count4;
                    // TODO: 发送ajax请求
                }


                function debounce3(func, wait = 1000, immediate = false) {
                    let timeout;

                    return function () {
                        let that = this;
                        let args = arguments;
                        if (timeout) {
                            clearTimeout(timeout);
                        }

                        // 不立刻执行, 和之前一样
                        if (!immediate) {
                            console.log("不立即执行");
                            timeout = setTimeout(function () {
                                func.apply(args);
                            }, wait);
                        } else {
                            // 立即执行
                            console.log("立即执行");

                            let rightNow = !timeout; // 通过该参数控制只执行一次

                            // 设置定时清理
                            timeout = setTimeout(function () {
                                timeout = null;
                            }, wait);

                            if (rightNow) {
                                func.apply(that, args);
                            }
                        }
                    }
                }

                eSearch4.addEventListener("keyup", debounce3(onSearch4, 1000, true))


                // -----------------------------------------------------
                // debounce3必须等n秒后才能能重新触发,现在希望能够取消防抖,让它能够立刻恢复执行

                let count5 = 0;
                let eSearch5 = document.getElementById("search5");
                let eResult5 = document.getElementById("result5");
                let eBtn1 = document.getElementById("btn1");

                function onSearch5() {
                    // input对象
                    console.log(this);

                    // undefined
                    console.log(event);

                    eResult5.innerHTML = ++count5;
                    // TODO: 发送ajax请求
                }


                function debounce4(func, wait = 1000, immediate = false) {
                    let timeout;
                    let debounce = function () {
                        let that = this;
                        let args = arguments;
                        if (timeout) clearTimeout(timeout);

                        // 立刻执行
                        if (immediate) {
                            let rightNow = !timeout; // 通过该参数控制只执行一次
                            timeout = setTimeout(function () {
                                timeout = null;
                            }, wait);
                            if (rightNow) {
                                func.apply(that, args);
                            }
                        } else {
                            timeout = setTimeout(function () {
                                func.apply(that, args);
                            }, wait);
                        }
                    };
                    // 取消
                    debounce.kkk = function () {
                        console.log("这是cancel");
                        clearTimeout(timeout);
                        timeout = null;
                    }
                    return debounce;
                }

                let doSearch = debounce4(onSearch5, 8000, true)


                eSearch5.addEventListener("keyup", doSearch);
                eBtn1.addEventListener("click", doSearch.kkk)


            </script>
        </div>
        <div>
</body>

</html>

参考资料

《前端进阶之道》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值