zoukankan      html  css  js  c++  java
  • 白话再谈防抖与节流--不蒙圈指北

    再谈防抖/节流

    对于一名合格的前端工程师,
    没有不知道防抖与节流的吧。

    我并不是一名真正的前端,
    正处于成为合格前端的路上。

    在前端的学习过程中,关于[防抖/节流]
    也听老师讲过,也看过一些大佬写的指南,
    虽然当时明白,过后即忘;
    关键在于理解和思路,今天整理一下,
    写一篇不懵圈指北分享给大家。

    防抖

    什么是防抖

    我有两个儿子,大宝跟二仁。
    这二人都是吃货。一天,

    场景1

    我 :大宝,爸爸今天升级加薪
       迎娶白富 美了,给你买好吃的,
       想吃点什么水果,选一个?
    大宝:我想吃,西瓜,
       不对,是哈密瓜,
       不对,是芒果,
       不对,是荔枝,
       不对,是草莓。
    我 :好的,草莓是吧,这就去买。

    这就是【防抖】,永远只响应最新的(最后一次)请求。

    防抖的JS实现

    实现思路

    • 实现一个防抖函数,作为包装器
    • 接收两个参数:
      实际要执行的函数(回调)
      要延迟的时间限制
    • 利用延时定时器,创造异步执行
      如果已有定时器,则清空定时器
      设置定时器,到执行时,清空当前定时器重新定时
    • 返回包装后的响应函数

    代码

    Talk is cheap,show you the code

    /**
     * 简单的`debounce`函数实现
     *
     * @param {function} cb 要执行的回调函数
     * @param {number} delay 要等待的防抖延迟时间
     * @returns {function}
     */
    const debounce = function(cb, delay) {
        // 参数检查
        //   cb:function
        //   delay:number
        if (!cb || toString.call(cb) !== '[object Function]') {
            throw new Error(`${cb} is not a function.`)
        }
        // 没有传 delay 参数时(包括等于0)
        if (!delay) {
            delay = 500 // 设置默认延时
        }
        // delay 参数须为正整数
        else if (!Number.isInteger(delay) || delay < 0) {
            throw new Error(`${delay} is invalid as optional parameter [delay]`)
        }
    
        // 定时器
        let timer = null
        
        // 返回debounce包装过的执行函数
        return function(...args) {
            // 如果存在定时器
            if (timer) {
                // 清除定时器,
                // 即:忽略之前的触发
                clearTimeout(timer)
            }
    
            // 设置定时器
            timer = setTimeout(() => {
                // 当到了设定的时间:
                //   清除本次定时器,
                //   并执行函数
                clearTimeout(timer)
                timer = null
                cb.call(null, ...args)
            }, delay);
        }
    }
    

    防抖典型的应用场景:
     输入框的提示或搜索功能

    如果随着输入实时检索,
    将白费很多次请求;

    这样利用防抖函数,可以设定500毫秒的延迟,当用户输入时不进行实时检索,超过500毫秒没有输入(停顿了)时,再发起检索请求,减少无谓的请求数量,节省网络和服务器资源。

    放个窗口滚动的防抖示例,如下:

    创建一个debounce_sample.html
    chrome 浏览器打开,甩起你的滚动轮并查看 console,自己感受一下。

    <!DOCTYPE html>
    <html>
    
    <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 sample</title>
    </head>
    
    <body>
        <h3>
            <ul>
                <li>打开console</li>
                <li>快速滚动鼠标滚动轮</li>
            </ul>
        </h3>
        <div style="height: 1500px;"></div>
    
        <script>
            /**
             * 简单的`debounce`函数实现
             *
             * @param {function} cb 要执行的回调函数
             * @param {number} delay 要等待的防抖延迟时间
             * @returns {function}
             */
            const debounce = function (cb, delay) {
                // ...此处省略以节省篇幅...
                // ...函数内容请参照上文...
            }
    
            // scroll in debounce
            const realEventHandler = function() {
                console.log('scroll in debounce')
            }
            const debouncedScrollHandler = debounce(realEventHandler, 500)
            window.addEventListener('scroll', debouncedScrollHandler)
        </script>
    </body>
    
    </html>
    

    节流

    什么是节流

    场景2

    [第一天]
    二仁:靶鼻,我好想吃冰淇淋,
       给我买一个好吗?
    我 :好吧,那就买一个,别告诉你哥。

    [第二天]
    二仁:靶鼻,我今天还想吃冰淇淋,
       再给我买一个好吗?
    我 :想啥呢二仁。。。
       哪能天天吃?!!
       咱家经济状况你又不是不知道,
       再加上你出生后,更揭不开锅了,
       你也知道,
       你现在喝的一段奶粉是最贵的,
       你哥幼儿园一个月两千多,
       你爸我一个月才几千工资啊,
       还得养活全家,还房贷交房租少吗,
       balabala。。。
       (总之没买)
    二仁 :好吧。。。

    [第三天]
    二仁:靶鼻,我有个小心愿,
       不是当讲不当讲。。。
    我 :说来听听
    二仁:就昨天那事儿,
       我还是想吃冰淇淋,
       再给我买一个好吗?
    我 :看你可怜的样,今天我就答应你。
       回去可得让我尝尝,另外,
       悄悄地吃别让你哥看见。

    [第四天]
    二仁:靶鼻,我想吃冰淇淋。。。
    我 :不行。
       昨天刚吃了。
       明天再说。

    这就是【节流】,

    • 一定时间内,只响应一次请求。
    • 经过该给定时间间隔之后,才能再次响应。

    节流的JS实现

    实现思路

    为了更方便理解,我们可以参考游戏里放大招,眼看敌人残血了,把握好机会立马放大准备收人头,说正事儿,放完大招,要等一个冷却时间过了,才能再次使用。

    节流throttle看起来也是这么回事儿,请看下面的代码实现(包括注释):

    代码

    /**
     * `throttle`简单的节流函数实现
     *
     * @param {function} cb 要执行的回调函数
     * @param {number} wait 要设置的节流时间间隔
     * @returns {function}
     */
    const throttle = function (cb, wait) {
        // 参数检查
        //   cb:function
        //   wait:number
        if (!cb || toString.call(cb) !== '[object Function]') {
            throw new Error(`${cb} is not a function.`)
        }
        // 没有传 wait 参数时(包括等于0)
        if (!wait) {
            wait = 500 // 设置默认延时
        }
        // wait 参数须为正整数
        else if (!Number.isInteger(wait) || wait < 0) {
            throw new Error(`${wait} is invalid as optional parameter [wait]`)
        }
        
        // 用来记录上次执行的时刻
        let lasttime = Date.now()
    
        return function (...args) {
            const now = Date.now()
            // 两次执行的时间间隔
            const timespan = now - lasttime
            // 当间隔小于等待时间即处于冷却中
            const isCoolingDown = timespan < wait
    
            console.log(timespan, isCoolingDown ? 'is cooling down' : 'execute')
    
            // 如果还没冷却好,就等待
            if (isCoolingDown) return
    
            // 记录本次执行的时刻
            lasttime = Date.now()
    
            // 冷却好了
            cb.apply(null, args)
        }
    }
    

    节流用在resize或者鼠标拖动之类的事件上是合适的,因为如果没有节流,体验会变得很糟糕。

    下面我们创建一个throttle_sample.html来体验一下效果。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>throttle sample</title>
    </head>
    <body>
        <h3>
            <ul>
                <li>打开console</li>
                <li>快速滚动鼠标滚动轮</li>
            </ul>
        </h3>
        <div style="height: 1500px;"></div>
    
        <script src="throttle.js"></script>
        <script>
            const realEventHandler = function() {
                console.log('scroll in throttle')
            }
            const scrollHandler = throttle(realEventHandler, 2000)  // 两秒钟内只响应一次(打印一次log)
            window.addEventListener('scroll', scrollHandler)
        </script>
    </body>
    </html>
    

    防抖+节流

    如果你亲自体验了上面防抖的示例,可能会发现这样一个问题:

    当我一直滚动鼠标滚动轮不松手时,那就一直不会触发事件,耗一年也不会。
    这,是不是问题?
    在实际场景中,这,确实是个问题。

    解决场景1存在的问题

    场景1问题

    我 :大宝,爸爸今天升级加薪
       迎娶白富 美了,给你买好吃的,
       想吃点什么水果,选一个?
    大宝:我想吃,西瓜,
       不对,是哈密瓜,
       不对,是樱桃,
       不对,是葡萄,
       不对,是橙子,
       不对,是香蕉,
       不对,是芒果,
       不对,是荔枝,
       。。。
      (一个小时过去了
       。。。
       不对,是草莓,
    我 :先停,谁家爸爸这么有耐心,
       都听你你说一个小时了,
       就按最后一个也就是第1024个:
       【草莓】,这就去买。

    解决办法:防抖 + 节流

    这就是【防抖 + 节流】,

    • 在规定的时间内,
      只响应最后一次请求,之前的都忽略
    • 只响应最后一次请求,
      规定的时间内,必须要响应一次

    说人话:

    防抖:只响应最后一次请求
    节流:单位时间间隔内只响应一次
    --> 只响应单位间隔期间里最后一次请求

    实现一个【防抖 + 节流】

    这里有一点不太好理解的地方,
    就是需要两个定时器,
    分别记录 防抖/节流。

    • 如果没有节流函数定时器,且超出规定的时间间隔,说明用户操作没有中断,此时需要强制执行一次回调函数响应
    • 其他情况,则按照防抖函数处理
    • 注意执行回调函数响应时,两个定时器都要清空
    /**
     * 防抖+节流的组合实现
     *
     * @param {function} cb 要执行的回调函数
     * @param {number} wait 要设置的防抖节流时间
     * @returns {function}
     * 
     * 思路:
     *   1. 第一次触发或者一直触发,考虑 throttle,定时响应一次(强制响应)
     *   2. 如果没有一直触发,则使用 debounce,响应最后一次请求
     */
    const combinedDebounceThrottle = function(cb, wait) {
        // TODO:参数检查
        //   cb:function
        //   wait:number
        // 略(参照 debounce.js 或 throttle.js 部分)
    
        let lasttime = 0
        let timerDebounce = null
        let timerThrottle = null
    
        // 执行一次回调响应的处理部分
        function executeCb(...args) {
            clearTimeout(timerDebounce)
            clearTimeout(timerThrottle)
            timerDebounce = null
            timerThrottle = null
            lasttime = Date.now()
            cb.apply(null, args)
        }
    
        return function(...args) {
            const now = Date.now()
            const timespan = now - lasttime
            const isCoolingDown = timespan < wait
    
            clearTimeout(timerDebounce)
    
            // 如果一直 debounce 而没有执行响应,
            // 且,超过冷却时间,则强制执行一次
            if (!timerThrottle && !isCoolingDown) {
                timerThrottle = setTimeout(executeCb, wait, ...args)
            }
            // 如果不是一直触发,则在延迟时间后做一次响应(使用debounce)
            else {
                timerDebounce = setTimeout(executeCb, wait, ...args)
            }
        }
    }
    

    还是按照惯例,创建一个combined_sample.html文件体验效果:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>throttle sample</title>
    </head>
    <body>
        <h3>
            <ul>
                <li>打开console</li>
                <li>快速滚动鼠标滚动轮</li>
            </ul>
        </h3>
        <div style="height: 1500px;"></div>
    
        <script src="combinedDebounceThrottle.js"></script>
        <script>
            const realEventHandler = function(e) {
                console.log('scroll in debounce-throttle combined mode', e)
            }
            const scrollHandler = combinedDebounceThrottle(realEventHandler, 2000)  // 两秒钟内只响应一次(打印一次log)
            window.addEventListener('scroll', scrollHandler)
        </script>
    </body>
    </html>
    

    可以看到,即便一直滚动,也会在2秒后打印出控制台信息。
    这样就实现了防抖+节流的组合模式。

    白话了半天,读者朋友们是不是都理解了?

    其他补充

    underscore.js中为我们提供了功能更为丰富的防抖与节流函数。
    是不是在想:你怎么不早说?!
    实现得越复杂就越不容易理解嘛~现在看了我的讲解,再去看 underscore.js 库里封装的优秀方法,也就能更好地理解了。

    underscore 里的防抖与节流,为我们提供了更为丰富的配置选项和自定义的可能,能够应对更多的业务场景,下面就来探究一下 underscore 中是怎样的吧。

    防抖debounce

    _.debounce(func, wait, [immediate])
    
    参数 说明
    function 处理函数
    wait 指定的毫秒数间隔
    immediate 立即执行Flag
    可选。
    默认:false

    功能解析:

    前两个参数与前面介绍的throttle是一样的,第三个参数,
    immediate指定第一次触发或没有等待中的时候可以立即执行。

    知道了原理,我们来简单写一下代码实现。

    // debounce 防抖:
    // 用户停止输入&wait毫秒后,响应,
    // 或 immediate = true 时,没有等待的回调的话立即执行
    // 立即执行并不影响去设定时器延迟执行
    _.debounce = function(func, wait, immediate){
        var timer, result
    
        var later = function(...args){
            clearTimeout(timer)
            timer = null
            result = func.apply(null, args)
        }
    
        return function(...args){
            // 因为防抖是响应最新一次操作,所以清空之前的定时器
            if(timer) clearTimeout(timer)
    
            // 如果配置了 immediate = true
            if(immediate){
                // 没有定时函数等待执行才可以立即执行
                var callNow = !timer
    
                // 是否立即执行,并不影响设定时器的延迟执行
                timer = setTimeout(later, wait, ...args)
    
                if(callNow){
                    result = func.apply(null, args)
                }
            }
            else{
                timer = setTimeout(later, wait, ...args)
            }
    
            return result
        }
    }
    

    节流throttle

    _.throttle(function, wait, [options])
    
    参数 说明
    function 处理函数
    wait 指定的毫秒数间隔
    options 配置
    可选。
    默认:
    {
     leading: false,
     trailing: false  
    }

    针对第一次触发,

    leading : true 相当于先执行,再等待wait毫秒之后才可再次触发
    trailing : true 相当于先等待wait毫秒,后执行

    默认:
    leading : false => 阻止第一次触发时立即执行,等待wait毫秒才可触发
    trailing : false => 阻止第一次触发时的延迟执行,经过延迟的wait毫秒之后才可触发

    可能的配置方式:
    (区别在首次执行和先执行还是先等待)

    配置 结果


    {
      leading: false,
      trailing: false }                           
    第一次触发不执行,后面同普通throttle,执行 + 间隔wait毫秒
    {
      leading: true,
      trailing: false
    }
    第一次触发立即执行,后面同普通throttle,执行 + 间隔wait毫秒
    {
      leading: false,
      trailing: true
    }
    每次触发延迟执行,每次执行间隔wait毫秒
    {
      leading: true,
      trailing: true
    }
    每一次有效触发都会执行两次,先立即执行一次,后延时wait毫秒执行一次

    知道了原理,我们来简单写一下代码实现。

    _.now = Date.now
    
    _.throttle = function(func, wait, options){
        var lastTime = 0
        var timeOut = null
        var result
        if(!options){
            options = { leading: false, trailing: false }
        }
    
        return function(...args){  // 节流函数
            var now = _.now()
    
            // 首次执行看是否配置了 leading = false = 默认,阻止立即执行
            if(!lastTime && options.leading === false){
                lastTime = now
            }
            // 配置了 leading = true 时,初始值 lastTime = 0,即可以立即执行
    
            var remaining = lastTime + wait - now
            // > 0 即间隔内
            // < 0 即超出间隔时间
    
            // 超出间隔时间,或首次的立即执行
            if(remaining <= 0){     // trailing=false
                if(timeOut){
                    // 如果不是首次执行的情况,需要清空定时器
                    clearTimeout(timeOut)
                    timeOut = null
                }
                lastTime = now      // #
                result = func.apply(null, args)
            }
            else if(!timeOut && options.trailing !== false){    // leading
                // 没超出间隔时间,但配置了 leading=fasle 阻止了立即执行,
                // 即需要执行一次却还未执行,等待中,且配置了 trailing=true
                // 那就要在剩余等待毫秒时间后触发
                timeOut = setTimeout(()=>{
                    lastTime = options.leading === false ? 0 : _.now()      // # !lastTime 的判断中需要此处重置为0
                    timeOut = null
                    result = func.apply(null, args)
                }, remaining);
            }
    
            return result
        }
    }
    

    除了上文介绍的配置,还加入了可取消功能(cancel)(from 1.9.0)

    小结

    throttledebounce是解决请求和响应速度不匹配问题的两个方案

    二者的差异在于选择不同的策略

    • debounce的关注点是空闲的间隔时间,
    • throttle的关注点是连续的执行间隔时间。

    应用场景

    • 游戏设计,keydown事件
    • 文本输入、自动完成,keyup事件
    • 鼠标移动mousemove事件
    • DOM元素动态定位,window对象的resizescroll事件

    总结比较

    对于我们最开始的简单防抖/节流实现

    • 相同
      • debounce防抖与throttle节流都实现了单位时间内,函数只执行一次
    • 不同
      • debounce防抖:
        单位时间内,忽略前面的,响应最新的,并在延迟wait毫秒后执行
      • throttle节流:
        响应第一次的,单位时间内,不再响应,直到wait毫秒后才再响应

    以上。再说不懂防抖节流算我输。


    最后,感谢您的阅读和支持~

  • 相关阅读:
    保存在线的PDF文件
    官网下载Java
    可执行文件(ELF)格式的理解
    虚拟机不识别U盘
    安装和连接精简版sqlserver2008
    [ZT] Linuxfs Readinglist
    分析ext2文件系统磁盘分区结构
    wma/mp3等格式转换为apple有声电子书格式(m4b) 以及itunes导入码率设置
    Progressivedownload 对于文件格式的要求
    Scaling Redis
  • 原文地址:https://www.cnblogs.com/CoderMonkie/p/cheap-talk-of-debounce-and-throttle.html
Copyright © 2011-2022 走看看