日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 综合教程 >内容正文

综合教程

白话再谈防抖与节流--不蒙圈指北

發(fā)布時間:2023/12/13 综合教程 25 生活家
生活随笔 收集整理的這篇文章主要介紹了 白话再谈防抖与节流--不蒙圈指北 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

再談防抖/節(jié)流

對于一名合格的前端工程師,
沒有不知道防抖與節(jié)流的吧。

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

在前端的學(xué)習(xí)過程中,關(guān)于[防抖/節(jié)流]
也聽老師講過,也看過一些大佬寫的指南,
雖然當(dāng)時明白,過后即忘;
關(guān)鍵在于理解和思路,今天整理一下,
寫一篇不懵圈指北分享給大家。

防抖

什么是防抖

我有兩個兒子,大寶跟二仁。
這二人都是吃貨。一天,

場景1

我?:大寶,爸爸今天升級加薪
???迎娶白富 美了,給你買好吃的,
???想吃點什么水果,選一個?
大寶:我想吃,西瓜,
???不對,是哈密瓜,
???不對,是芒果,
???不對,是荔枝,
???不對,是草莓。
我?:好的,草莓是吧,這就去買。

這就是【防抖】,永遠只響應(yīng)最新的(最后一次)請求。

防抖的JS實現(xiàn)

實現(xiàn)思路

實現(xiàn)一個防抖函數(shù),作為包裝器
接收兩個參數(shù):
實際要執(zhí)行的函數(shù)(回調(diào))
要延遲的時間限制
利用延時定時器,創(chuàng)造異步執(zhí)行
如果已有定時器,則清空定時器
設(shè)置定時器,到執(zhí)行時,清空當(dāng)前定時器重新定時
返回包裝后的響應(yīng)函數(shù)

代碼

Talk is cheap,show you the code

/**
 * 簡單的`debounce`函數(shù)實現(xiàn)
 *
 * @param {function} cb 要執(zhí)行的回調(diào)函數(shù)
 * @param {number} delay 要等待的防抖延遲時間
 * @returns {function}
 */
const debounce = function(cb, delay) {
    // 參數(shù)檢查
    //   cb:function
    //   delay:number
    if (!cb || toString.call(cb) !== '[object Function]') {
        throw new Error(`${cb} is not a function.`)
    }
    // 沒有傳 delay 參數(shù)時(包括等于0)
    if (!delay) {
        delay = 500 // 設(shè)置默認延時
    }
    // delay 參數(shù)須為正整數(shù)
    else if (!Number.isInteger(delay) || delay < 0) {
        throw new Error(`${delay} is invalid as optional parameter [delay]`)
    }

    // 定時器
    let timer = null
    
    // 返回debounce包裝過的執(zhí)行函數(shù)
    return function(...args) {
        // 如果存在定時器
        if (timer) {
            // 清除定時器,
            // 即:忽略之前的觸發(fā)
            clearTimeout(timer)
        }

        // 設(shè)置定時器
        timer = setTimeout(() => {
            // 當(dāng)?shù)搅嗽O(shè)定的時間:
            //   清除本次定時器,
            //   并執(zhí)行函數(shù)
            clearTimeout(timer)
            timer = null
            cb.call(null, ...args)
        }, delay);
    }
}

防抖典型的應(yīng)用場景:
?輸入框的提示或搜索功能

如果隨著輸入實時檢索,
將白費很多次請求;

這樣利用防抖函數(shù),可以設(shè)定500毫秒的延遲,當(dāng)用戶輸入時不進行實時檢索,超過500毫秒沒有輸入(停頓了)時,再發(fā)起檢索請求,減少無謂的請求數(shù)量,節(jié)省網(wǎng)絡(luò)和服務(wù)器資源。

放個窗口滾動的防抖示例,如下:

創(chuàng)建一個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></div>

    <script>
        /**
         * 簡單的`debounce`函數(shù)實現(xiàn)
         *
         * @param {function} cb 要執(zhí)行的回調(diào)函數(shù)
         * @param {number} delay 要等待的防抖延遲時間
         * @returns {function}
         */
        const debounce = function (cb, delay) {
            // ...此處省略以節(jié)省篇幅...
            // ...函數(shù)內(nèi)容請參照上文...
        }

        // scroll in debounce
        const realEventHandler = function() {
            console.log('scroll in debounce')
        }
        const debouncedScrollHandler = debounce(realEventHandler, 500)
        window.addEventListener('scroll', debouncedScrollHandler)
    </script>
</body>

</html>

節(jié)流

什么是節(jié)流

場景2

[第一天]
二仁:靶鼻,我好想吃冰淇淋,
???給我買一個好嗎?
我?:好吧,那就買一個,別告訴你哥。

[第二天]
二仁:靶鼻,我今天還想吃冰淇淋,
???再給我買一個好嗎?
我?:想啥呢二仁。。。
???哪能天天吃?!!
???咱家經(jīng)濟狀況你又不是不知道,
???再加上你出生后,更揭不開鍋了,
???你也知道,
???你現(xiàn)在喝的一段奶粉是最貴的,
???你哥幼兒園一個月兩千多,
???你爸我一個月才幾千工資啊,
???還得養(yǎng)活全家,還房貸交房租少嗎,
???balabala。。。
???(總之沒買)
二仁?:好吧。。。

[第三天]
二仁:靶鼻,我有個小心愿,
???不是當(dāng)講不當(dāng)講。。。
我?:說來聽聽
二仁:就昨天那事兒,
???我還是想吃冰淇淋,
???再給我買一個好嗎?
我?:看你可憐的樣,今天我就答應(yīng)你。
???回去可得讓我嘗嘗,另外,
???悄悄地吃別讓你哥看見。

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

這就是【節(jié)流】,

一定時間內(nèi),只響應(yīng)一次請求。
經(jīng)過該給定時間間隔之后,才能再次響應(yīng)。

節(jié)流的JS實現(xiàn)

實現(xiàn)思路

為了更方便理解,我們可以參考游戲里放大招,眼看敵人殘血了,把握好機會立馬放大準備收人頭,說正事兒,放完大招,要等一個冷卻時間過了,才能再次使用。

節(jié)流throttle看起來也是這么回事兒,請看下面的代碼實現(xiàn)(包括注釋):

代碼

/**
 * `throttle`簡單的節(jié)流函數(shù)實現(xiàn)
 *
 * @param {function} cb 要執(zhí)行的回調(diào)函數(shù)
 * @param {number} wait 要設(shè)置的節(jié)流時間間隔
 * @returns {function}
 */
const throttle = function (cb, wait) {
    // 參數(shù)檢查
    //   cb:function
    //   wait:number
    if (!cb || toString.call(cb) !== '[object Function]') {
        throw new Error(`${cb} is not a function.`)
    }
    // 沒有傳 wait 參數(shù)時(包括等于0)
    if (!wait) {
        wait = 500 // 設(shè)置默認延時
    }
    // wait 參數(shù)須為正整數(shù)
    else if (!Number.isInteger(wait) || wait < 0) {
        throw new Error(`${wait} is invalid as optional parameter [wait]`)
    }
    
    // 用來記錄上次執(zhí)行的時刻
    let lasttime = Date.now()

    return function (...args) {
        const now = Date.now()
        // 兩次執(zhí)行的時間間隔
        const timespan = now - lasttime
        // 當(dāng)間隔小于等待時間即處于冷卻中
        const isCoolingDown = timespan < wait

        console.log(timespan, isCoolingDown ? 'is cooling down' : 'execute')

        // 如果還沒冷卻好,就等待
        if (isCoolingDown) return

        // 記錄本次執(zhí)行的時刻
        lasttime = Date.now()

        // 冷卻好了
        cb.apply(null, args)
    }
}

節(jié)流用在resize或者鼠標拖動之類的事件上是合適的,因為如果沒有節(jié)流,體驗會變得很糟糕。

下面我們創(chuàng)建一個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></div>

    <script src="throttle.js"></script>
    <script>
        const realEventHandler = function() {
            console.log('scroll in throttle')
        }
        const scrollHandler = throttle(realEventHandler, 2000)  // 兩秒鐘內(nèi)只響應(yīng)一次(打印一次log)
        window.addEventListener('scroll', scrollHandler)
    </script>
</body>
</html>

防抖+節(jié)流

如果你親自體驗了上面防抖的示例,可能會發(fā)現(xiàn)這樣一個問題:

當(dāng)我一直滾動鼠標滾動輪不松手時,那就一直不會觸發(fā)事件,耗一年也不會。
這,是不是問題?
在實際場景中,這,確實是個問題。

解決場景1存在的問題

場景1問題

我?:大寶,爸爸今天升級加薪
???迎娶白富 美了,給你買好吃的,
???想吃點什么水果,選一個?
大寶:我想吃,西瓜,
???不對,是哈密瓜,
???不對,是櫻桃,
???不對,是葡萄,
???不對,是橙子,
???不對,是香蕉,
???不對,是芒果,
???不對,是荔枝,
???。。。
??(一個小時過去了
???。。。
???不對,是草莓,
我?:先停,誰家爸爸這么有耐心,
???都聽你你說一個小時了,
???就按最后一個也就是第1024個:
???【草莓】,這就去買。

解決辦法:防抖 + 節(jié)流

這就是【防抖 + 節(jié)流】,

在規(guī)定的時間內(nèi),
只響應(yīng)最后一次請求,之前的都忽略
只響應(yīng)最后一次請求,
規(guī)定的時間內(nèi),必須要響應(yīng)一次

說人話:

防抖:只響應(yīng)最后一次請求
節(jié)流:單位時間間隔內(nèi)只響應(yīng)一次
--> 只響應(yīng)單位間隔期間里最后一次請求

實現(xiàn)一個【防抖 + 節(jié)流】

這里有一點不太好理解的地方,
就是需要兩個定時器,
分別記錄 防抖/節(jié)流。

如果沒有節(jié)流函數(shù)定時器,且超出規(guī)定的時間間隔,說明用戶操作沒有中斷,此時需要強制執(zhí)行一次回調(diào)函數(shù)響應(yīng)
其他情況,則按照防抖函數(shù)處理
注意執(zhí)行回調(diào)函數(shù)響應(yīng)時,兩個定時器都要清空

/**
 * 防抖+節(jié)流的組合實現(xiàn)
 *
 * @param {function} cb 要執(zhí)行的回調(diào)函數(shù)
 * @param {number} wait 要設(shè)置的防抖節(jié)流時間
 * @returns {function}
 * 
 * 思路:
 *   1. 第一次觸發(fā)或者一直觸發(fā),考慮 throttle,定時響應(yīng)一次(強制響應(yīng))
 *   2. 如果沒有一直觸發(fā),則使用 debounce,響應(yīng)最后一次請求
 */
const combinedDebounceThrottle = function(cb, wait) {
    // TODO:參數(shù)檢查
    //   cb:function
    //   wait:number
    // 略(參照 debounce.js 或 throttle.js 部分)

    let lasttime = 0
    let timerDebounce = null
    let timerThrottle = null

    // 執(zhí)行一次回調(diào)響應(yīng)的處理部分
    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 而沒有執(zhí)行響應(yīng),
        // 且,超過冷卻時間,則強制執(zhí)行一次
        if (!timerThrottle && !isCoolingDown) {
            timerThrottle = setTimeout(executeCb, wait, ...args)
        }
        // 如果不是一直觸發(fā),則在延遲時間后做一次響應(yīng)(使用debounce)
        else {
            timerDebounce = setTimeout(executeCb, wait, ...args)
        }
    }
}

還是按照慣例,創(chuàng)建一個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></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)  // 兩秒鐘內(nèi)只響應(yīng)一次(打印一次log)
        window.addEventListener('scroll', scrollHandler)
    </script>
</body>
</html>

可以看到,即便一直滾動,也會在2秒后打印出控制臺信息。
這樣就實現(xiàn)了防抖+節(jié)流的組合模式。

白話了半天,讀者朋友們是不是都理解了?

其他補充

underscore.js中為我們提供了功能更為豐富的防抖與節(jié)流函數(shù)。
是不是在想:你怎么不早說?!
實現(xiàn)得越復(fù)雜就越不容易理解嘛~現(xiàn)在看了我的講解,再去看 underscore.js 庫里封裝的優(yōu)秀方法,也就能更好地理解了。

underscore 里的防抖與節(jié)流,為我們提供了更為豐富的配置選項和自定義的可能,能夠應(yīng)對更多的業(yè)務(wù)場景,下面就來探究一下 underscore 中是怎樣的吧。

防抖debounce

_.debounce(func, wait, [immediate])
參數(shù) 說明
function 處理函數(shù)
wait 指定的毫秒數(shù)間隔
immediate 立即執(zhí)行Flag
可選。
默認:false

功能解析:

前兩個參數(shù)與前面介紹的throttle是一樣的,第三個參數(shù),
immediate指定第一次觸發(fā)或沒有等待中的時候可以立即執(zhí)行。

知道了原理,我們來簡單寫一下代碼實現(xiàn)。

// debounce 防抖:
// 用戶停止輸入&wait毫秒后,響應(yīng),
// 或 immediate = true 時,沒有等待的回調(diào)的話立即執(zhí)行
// 立即執(zhí)行并不影響去設(shè)定時器延遲執(zhí)行
_.debounce = function(func, wait, immediate){
    var timer, result

    var later = function(...args){
        clearTimeout(timer)
        timer = null
        result = func.apply(null, args)
    }

    return function(...args){
        // 因為防抖是響應(yīng)最新一次操作,所以清空之前的定時器
        if(timer) clearTimeout(timer)

        // 如果配置了 immediate = true
        if(immediate){
            // 沒有定時函數(shù)等待執(zhí)行才可以立即執(zhí)行
            var callNow = !timer

            // 是否立即執(zhí)行,并不影響設(shè)定時器的延遲執(zhí)行
            timer = setTimeout(later, wait, ...args)

            if(callNow){
                result = func.apply(null, args)
            }
        }
        else{
            timer = setTimeout(later, wait, ...args)
        }

        return result
    }
}

節(jié)流throttle

_.throttle(function, wait, [options])
參數(shù) 說明
function 處理函數(shù)
wait 指定的毫秒數(shù)間隔
options 配置
可選。
默認:
{
?leading: false,
?trailing: false ?
}

針對第一次觸發(fā),

leading : true 相當(dāng)于先執(zhí)行,再等待wait毫秒之后才可再次觸發(fā)
trailing : true 相當(dāng)于先等待wait毫秒,后執(zhí)行

默認:
leading : false => 阻止第一次觸發(fā)時立即執(zhí)行,等待wait毫秒才可觸發(fā)
trailing : false => 阻止第一次觸發(fā)時的延遲執(zhí)行,經(jīng)過延遲的wait毫秒之后才可觸發(fā)

可能的配置方式:
(區(qū)別在首次執(zhí)行和先執(zhí)行還是先等待)

配置 結(jié)果

{
leading: false,
trailing: false }?????????????????????????

第一次觸發(fā)不執(zhí)行,后面同普通throttle,執(zhí)行 + 間隔wait毫秒
{
leading: true,
trailing: false
}
第一次觸發(fā)立即執(zhí)行,后面同普通throttle,執(zhí)行 + 間隔wait毫秒
{
leading: false,
trailing: true
}
每次觸發(fā)延遲執(zhí)行,每次執(zhí)行間隔wait毫秒
{
leading: true,
trailing: true
}
每一次有效觸發(fā)都會執(zhí)行兩次,先立即執(zhí)行一次,后延時wait毫秒執(zhí)行一次

知道了原理,我們來簡單寫一下代碼實現(xiàn)。

_.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){  // 節(jié)流函數(shù)
        var now = _.now()

        // 首次執(zhí)行看是否配置了 leading = false = 默認,阻止立即執(zhí)行
        if(!lastTime && options.leading === false){
            lastTime = now
        }
        // 配置了 leading = true 時,初始值 lastTime = 0,即可以立即執(zhí)行

        var remaining = lastTime + wait - now
        // > 0 即間隔內(nèi)
        // < 0 即超出間隔時間

        // 超出間隔時間,或首次的立即執(zhí)行
        if(remaining <= 0){     // trailing=false
            if(timeOut){
                // 如果不是首次執(zhí)行的情況,需要清空定時器
                clearTimeout(timeOut)
                timeOut = null
            }
            lastTime = now      // #
            result = func.apply(null, args)
        }
        else if(!timeOut && options.trailing !== false){    // leading
            // 沒超出間隔時間,但配置了 leading=fasle 阻止了立即執(zhí)行,
            // 即需要執(zhí)行一次卻還未執(zhí)行,等待中,且配置了 trailing=true
            // 那就要在剩余等待毫秒時間后觸發(fā)
            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)

小結(jié)

throttledebounce是解決請求和響應(yīng)速度不匹配問題的兩個方案

二者的差異在于選擇不同的策略

debounce的關(guān)注點是空閑的間隔時間,
throttle的關(guān)注點是連續(xù)的執(zhí)行間隔時間。

應(yīng)用場景

游戲設(shè)計,keydown事件
文本輸入、自動完成,keyup事件
鼠標移動mousemove事件
DOM元素動態(tài)定位,window對象的resizescroll事件

總結(jié)比較

對于我們最開始的簡單防抖/節(jié)流實現(xiàn)

相同

debounce防抖與throttle節(jié)流都實現(xiàn)了單位時間內(nèi),函數(shù)只執(zhí)行一次

不同

debounce防抖:
單位時間內(nèi),忽略前面的,響應(yīng)最新的,并在延遲wait毫秒后執(zhí)行
throttle節(jié)流:
響應(yīng)第一次的,單位時間內(nèi),不再響應(yīng),直到wait毫秒后才再響應(yīng)

以上。再說不懂防抖節(jié)流算我輸。


最后,感謝您的閱讀和支持~

總結(jié)

以上是生活随笔為你收集整理的白话再谈防抖与节流--不蒙圈指北的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。