zoukankan      html  css  js  c++  java
  • JavaScript 防抖和节流

    JS 防抖和节流

    防抖和节流 anti-shake and throttling


    防抖

    防抖:是指在事件触发后的n秒内,该功能(事件)只能执行一次。如果在n秒内再次出发该事件,或者反复出发该事件,将阻止该事件。通常使用 setTimeout 重新计算函数执行时间。

    常见的场景包括:

    • 文本框智能提示/自动补全(keyup):比如搜索软件的搜索框,输入“google”,会自动关联“google play”、“google play store”、“google chrome”等,如果用户输入太快,每按一个键都去后台搜索一次,则会浪费很多带宽资源。我们希望延迟一会儿,让用户把关键词输入完整之后再去搜索。
    • 浏览器窗口调整大小(resize)或鼠标移动(mousemove)等事件:前段会有调整浏览器窗口大小的时候触发某个事件,或者鼠标移动的时候触发某个事件,但这些事件会非常频繁的触发,会导致网页特别的卡,并且我们完全不需要如此频繁的处罚,我们希望延迟几秒后再触发。
    • 查询或提交按钮(click):阻止过于频繁的点击查询按钮。

    实现方法:使用 setTimeout 延迟事件触发,期间有新的事件触发则重新刷新 delay time。并且需要使用闭包(closure)保存 timeoutID


    测试没有防抖

    用一个按钮来测试是最简单的,其他功能都会增加额外的代码。

    测试下面这段代码,打开浏览器开发者工具(F12),查看输入,每点一次按钮都会在控制台输出两段文本。

    <input type="button" id="btn" value="console">
    <script>
        document.querySelector('#btn').addEventListener('click', consoleTimeNow)
    
        function consoleTimeNow() {
            console.log('Hi June.')
            console.log('Long time no see and i miss you very much.')
            console.log(new Date())
        }
    </script>
    

    假设这个按钮每点一次按钮都是一次 ajax 查询,那么就会造成不断的请求网络。


    添加防抖函数

    假设学校的选修课申请页面,在姓名这一栏没有输入完整的情况下,会自动模糊查询,用户在没有输入完整的情况下就可以匹配完整的结果。一个学生输入“Trump”之后,自动匹配“Donald John Trump”。

    或者搜索引擎的搜索框,用户可能自己也不知道完整的关键字是什么,“智能提示”可以帮助到用户。

    实现这些功能我们会给这个文本框注册keyup事件,但这个事件在用户每次按下按键都会触发,用户输入“Trump”就触发了5次,这样频繁的发起请求,会给数据库和网络增加很多压力,实际上用户完全不需要如此频繁的“自动补全”,甚至用户在连续输入的时候,频繁的“智能提示/自动补全”可能会影响到用户原本连贯的输入,如果我们可以延迟一段时间触发事件,只有在用户输入一段文本停顿一下的时候才触发,那就完美了。

    所以我们需要实现的是每一次触发事件都延迟触发,在这期间再触发事件则重新计算延迟时间,直到用户停下输入,停止触发事件,不在刷新延迟时间,那在一段延迟之后,才会真正的触发事件。这就是防抖,这种情况非常适合“智能提示“和”自动补全”。

    实现方式很简单,使用setTimeout来延迟运行,重复触发则清除上一个 timerout再新建一个,这样就实现了每一次都延时并且重复触发则不断重新计算timeout。

    <input type="button" id="btn" value="console">
    <script>
        document.querySelector('#btn').addEventListener('click', debounce(consoleTimeNow))
    
        function consoleTimeNow() {
            console.log('Hi June.')
            console.log('Long time no see and i miss you very much.')
            console.log(new Date())
        }
    
        // 防抖函数
        function debounce(fn) {
            // 在函数外创建一个 timeoutId,这个id会被闭包保存
            var timerId
            return function () {
                if (timerId != null)
                    clearTimeout(timerId)
                timerId = setTimeout(fn, 1000)
            }
        }
    </script>
    

    这是防抖函数最核心的代码,有了思路之后,就是这么简单。


    回调函数传参 方法1

    添加传参时要注意,debounce 返回的嵌套函数是用来绑定事件的,所以这个嵌套函数不能接受我们传递的实参,前台函数的实参只可能是事件对象。我们要传参,需要从 debounce 函数传入,然后再又 debounce 内部 callback 的时候将函数传入 callback。

    所以,如果使用 function 函数嵌套,要注意我们要使用外层debounce 函数的作用域下的 arguments,而不是嵌套函数作用域下的 arguments,为此我们需要在外层函数将 arguments 保存下来,返回函数会通过闭包保存这个值。

    不过 ES6 的剪头函数并没有自己的作用域,它直接使用了父级函数的作用域,这样在剪头函数里头,可以直接使用 arguments 对象来读取实参。

    <input type="button" id="btn" value="console">
    <script>
        document.querySelector('#btn').addEventListener('click', debounce(consoleTimeNow, 'June'))
    
        function consoleTimeNow(args) {
            // 此时需要注意,args 是一个like array,第一个参数是callback function,第二个参数才是 "June"
            var name = args[1]
            console.log('Hi {0}.'.replace('{0}', name))
            console.log('Long time no see and i miss you very much.')
            console.log(new Date())
        }
    
        // 防抖函数
        function debounce(fn) {
            var timerId
            // 保存当前函数的形参,如果使用剪头函数则不需要在当前作用域保存。
            var args = arguments
            return function () {
                if (timerId != null)
                    clearTimeout(timerId)
    
                // setTimeout 第一个参数接受一个函数,需要给他一个函数指针
                // 此时 args 是外层作用域的 arguments,会被保存在闭包里
                // arguments 是一个 like array
                timerId = setTimeout(function () {
                    fn(args)
                }, 1000)
            }
        }
    </script>
    

    优化一下代码
    <input type="button" id="btn" value="console">
    <script>
        // 因为现在必须要传2个参数,fn和delay,所以如果想要把参数传入 callback,必须要从第3个参数开始传递
        // 如果只有一个参数(callback fn),第二个参数可以省略,因为防抖函数里设置了默认值
        document.querySelector('#btn').addEventListener('click', debounce(consoleTimeNow, 500, 'June'))
    
        function consoleTimeNow(name) {
            // 使用 apply 之后,会把数组扩展开
            console.log('Hi {0}.'.replace('{0}', name))
            console.log('Long time no see and i miss you very much.')
            console.log(new Date())
        }
    
        // 防抖函数-之前的版本改进
        function debounceES5(fn, delay) {
            // 默认 1000 毫秒
            delay = delay || 1000
            var timerId
    
            // 从第3个参数开始才是 callback function 的传入实参
            var args
            if (arguments.length > 2) {
                args = Array.prototype.slice.call(arguments).splice(2)
            }
    
            return function () {
                // 我们确定这个变量只会保存 timeoutId,所以简化一下完全不会有问题
                if (timerId) clearTimeout(timerId)
    
                var that = this
    
                timerId = setTimeout(function () {
                    // fn(args)
                    // 帮助调用对象绑定 this
                    fn.apply(that, args)
                }, delay)
            }
        }
    
    
        // 防抖函数-ES6版本
        // ES6 可以使用参数默认值 delay = 200
        function debounce(fn, delay = 200) {
            let timerId = null;
    
            // ES6 的箭头函数没有自己的作用域和this指针,使用的都是父级的,所以不需要闭包保存 arguments 和 this
            return () => {
                if (timerId) clearTimeout(timerId);
    
                timerId = setTimeout(() => {
                    // ES6可以使用扩展运算符
                    fn.apply(this, [...arguments].splice(2));
                }, delay);
            }
        }
    </script>
    

    后续的code都在 ES6的基础上改动。


    回调函数传参 方法2

    第一种方式并不好,有一种更好的解决方案是 debounce 函数不接受 callback function 的参数,而是当调用 debounce 函数的时候,通过 band 函数传递参数。

    <input type="button" id="btn" value="console">
    <script>
        /*| 关键方法在这里,
        |*| 使用 bing 来传递 callback function 参数,这样就可以不受 debounceES5 参数的影响
        |*| bind 还可以指定 this
        |*/
        var btn = document.querySelector('#btn')
        btn.addEventListener('click', debounce(consoleTimeNow).bind(btn, 'June'))
    
        /*| 也可以用以下方式调用
        |*| - 不需要绑定参数
        |*| document.querySelector('#btn').addEventListener('click', debounce(consoleTimeNow))
        |*| - 不需要绑定 this
        |*| document.querySelector('#btn').addEventListener('click', debounce(consoleTimeNow).bind(null, 'June'))
        |*/
        
        function consoleTimeNow(name) {
            console.log('Hi {0}.'.replace('{0}', name))
            console.log('Long time no see and i miss you very much.')
            console.log(new Date())
        }
    
        // 防抖函数-之前的版本改进
        function debounceES5(fn, delay) {
            delay = delay || 1000
            var timerId
    
            return function () {
                if (timerId) clearTimeout(timerId)
    
                var that = this
                // 和方法1相比,不使用 debounceES5 函数的作用域,使用嵌套函数的作用域,调用时会通过bind绑定参数
                var args = arguments
    
                timerId = setTimeout(function () {
                    fn.apply(that, args)
                }, delay)
            }
        }
    
    
        // 防抖函数-ES6版本
        function debounce(fn, delay = 1000) {
            let timerId = null;
    
            // 相比方法1,这里需要改成function函数,因为需要使用这个前台函数的this和arguments
            return function () {
                if (timerId) clearTimeout(timerId);
                // 这样就可以接受bing null,方便调用
                let that = this == null ? window : this
    
                // 这里要用箭头函数,使用的是它的父函数的this和arguments
                timerId = setTimeout(() => {
                    fn.apply(that, arguments);
                }, delay);
            }
        }
    </script>
    

    立即执行版本

    前面的代码是延迟执行,特别适合“自动填充”和“智能提示”。

    而现在希望事件被立即执行,但是执行之后一段时间内需要防抖处理,不能被重复点击。需要过一段时间之后才能继续触发事件。这种防抖比较适合“提交”和“查询”按钮的防抖。

    立即执行版本最简单的实现方式就是,回调函数立即执行而不是延迟执行,然后使用一个状态变量来确定是否允许执行,之前 setTimeout 那套逻辑用来控制状态变量。


    <input type="button" id="btn" value="console">
    <script>
        document.querySelector('#btn').addEventListener('click', debounceDelay(consoleTimeNow).bind(null, 'June'))
    
        function consoleTimeNow(name) {
            console.log('Hi {0}.'.replace('{0}', name))
            console.log('Long time no see and i miss you very much.')
            console.log(new Date())
        }
    
        // 防抖函数-ES5版本
        function debounceDelayEs5(fn, delay) {
            delay = delay || 1000
            var timerId
    
            return function () {
                if (timerId) clearTimeout(timerId)
    
                var that = this
                var args = arguments
    
                // 先计时再callback,计时需要刷新timerId,所以这里需要保存状态
                var allowRun = !timerId
    
                // 原本先是无脑的 create/clear timeout 来实现延迟的,timeoutId只是用来 clear的,并不需要用来判断状态
                // 但现在需要通过 timerId 来判断状态,所以执行完成之后必须要 timerId = null
                timerId = setTimeout(function () {
                    timerId = null
                }, delay)
    
                if (allowRun) fn.apply(that, args)
            }
        }
    
        // 防抖函数-ES6版本
        function debounceDelay(fn, delay = 1000) {
            let timerId = null;
    
            // ES6版本的改动和ES5版本没啥区别
            return function () {
                if (timerId) clearTimeout(timerId);
    
                let that = this == null ? window : this
                var allowRun = !timerId
    
                timerId = setTimeout(() => {
                    timerId = null
                }, delay);
    
                if (allowRun) fn.apply(that, arguments)
            }
        }
    </script>
    

    节流

    节流:是指一段时间内只能触发一次。在一段时间内连续触发同一个事件,触发一次之后会阻止后面的事件再次被触发。

    使用场景:比如查询按钮,查询可能需要2秒左右时间,避免用户在未出结果前多次点击查询按钮,限制用户2秒内只能查询一次。

    实现方式:防抖是使用 setTimeout,但是每次重复触发防抖函数都会重新刷新 timeout。节流其实差不多,也可以使用 setTimeout,但每次触发防抖函数不刷新 timeout,而是阻止再次 callback,直到限制时间结束。

    <input type="button" id="btn" value="console">
    <script>
        document.querySelector('#btn').addEventListener('click', throttle(consoleTimeNow, 2000).bind(null, 'June'))
    
        function consoleTimeNow(name) {
            console.log('Hi {0}.'.replace('{0}', name))
            console.log('long time no see and i miss you very much.')
            console.log(new Date())
        }
    
        // Throttle
        function throttle(fn, delay = 1000) {
            let allowRun = true
            return function () {
                if (!allowRun) return
    
                setTimeout(() => {
                    allowRun = true
                }, delay);
    
                allowRun = false
                fn.apply(this, arguments)
            }
        }
    </script>
    


  • 相关阅读:
    数组 滑动窗口
    爬虫案例 下载某文库付费文档 全格式
    双指针 三数之和
    双指针 四数之和
    双指针法 环形链表 II
    判断是否手机端
    C# 模拟点击
    chrome 扩展开发注意事项
    破解拖动验 证码
    //刷新任务栏图标 终止别的进程序有些程序有托盘会残留
  • 原文地址:https://www.cnblogs.com/luciolu/p/15217460.html
Copyright © 2011-2022 走看看