防抖和節(jié)流2020-08-26

防抖和節(jié)流嚴(yán)格算起來應(yīng)該屬于性能優(yōu)化的知識响鹃,但實(shí)際上遇到的頻率相當(dāng)高驾霜,處理不當(dāng)或者放任不管就容易引起瀏覽器卡死。所以還是很有必要早點(diǎn)掌握的买置。

從滾動條監(jiān)聽的例子說起

先說一個常見的功能粪糙,很多網(wǎng)站會提供這么一個按鈕:用于返回頂部。

這個按鈕只會在滾動到距離頂部一定位置之后才出現(xiàn)忿项,那么我們現(xiàn)在抽象出這個功能需求-- 監(jiān)聽瀏覽器滾動事件蓉冈,返回當(dāng)前滾條與頂部的距離
這個需求很簡單,直接寫:

function showTop  () {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滾動條位置:' + scrollTop);
}
window.onscroll  = showTop

但是轩触!

[圖片上傳失敗...(image-6e382d-1598417668185)]

在運(yùn)行的時(shí)候會發(fā)現(xiàn)存在一個問題:這個函數(shù)的默認(rèn)執(zhí)行頻率寞酿,太!高脱柱!了伐弹!。 高到什么程度呢榨为?以chrome為例掸茅,我們可以點(diǎn)擊選中一個頁面的滾動條,然后點(diǎn)擊一次鍵盤的【向下方向鍵】柠逞,會發(fā)現(xiàn)函數(shù)執(zhí)行了8-9次昧狮!
[圖片上傳失敗...(image-fb7a38-1598417668185)]

然而實(shí)際上我們并不需要如此高頻的反饋,畢竟瀏覽器的性能是有限的板壮,不應(yīng)該浪費(fèi)在這里逗鸣,所以接著討論如何優(yōu)化這種場景。

防抖(debounce)

基于上述場景,首先提出第一種思路:在第一次觸發(fā)事件時(shí)撒璧,不立即執(zhí)行函數(shù)透葛,而是給出一個期限值比如200ms,然后:

  • 如果在200ms內(nèi)沒有再次觸發(fā)滾動事件卿樱,那么就執(zhí)行函數(shù)
  • 如果在200ms內(nèi)再次觸發(fā)滾動事件僚害,那么當(dāng)前的計(jì)時(shí)取消,重新開始計(jì)時(shí)

效果:如果短時(shí)間內(nèi)大量觸發(fā)同一事件繁调,只會執(zhí)行一次函數(shù)萨蚕。

實(shí)現(xiàn):既然前面都提到了計(jì)時(shí),那實(shí)現(xiàn)的關(guān)鍵就在于setTimeOut這個函數(shù)蹄胰,由于還需要一個變量來保存計(jì)時(shí)岳遥,考慮維護(hù)全局純凈,可以借助閉包來實(shí)現(xiàn):

/*
* fn [function] 需要防抖的函數(shù)
* delay [number] 毫秒裕寨,防抖期限值
*/
function debounce(fn,delay){
    let timer = null //借助閉包
    return function() {
        if(timer){
            clearTimeout(timer) //進(jìn)入該分支語句浩蓉,說明當(dāng)前正在一個計(jì)時(shí)過程中,并且又觸發(fā)了相同事件宾袜。所以要取消當(dāng)前的計(jì)時(shí)捻艳,重新開始計(jì)時(shí)
            timer = setTimeOut(fn,delay) 
        }else{
            timer = setTimeOut(fn,delay) // 進(jìn)入該分支說明當(dāng)前并沒有在計(jì)時(shí),那么就開始一個計(jì)時(shí)
        }
    }
}

當(dāng)然 上述代碼是為了貼合思路庆猫,方便理解讯泣,寫完會發(fā)現(xiàn)其實(shí)time = setTimeOut(fn,delay)是一定會執(zhí)行的,所以可以稍微簡化下:


/*****************************簡化后的分割線 ******************************/
function debounce(fn,delay){
    let timer = null //借助閉包
    return function() {
        if(timer){
            clearTimeout(timer) 
        }
        timer = setTimeout(fn,delay) // 簡化寫法
    }
}
// 然后是舊代碼
function showTop  () {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滾動條位置:' + scrollTop);
}
window.onscroll = debounce(showTop,1000) // 為了方便觀察效果我們?nèi)€大點(diǎn)的間斷值阅悍,實(shí)際使用根據(jù)需要來配置

此時(shí)會發(fā)現(xiàn),必須在停止?jié)L動1秒以后昨稼,才會打印出滾動條位置节视。

到這里,已經(jīng)把防抖實(shí)現(xiàn)了假栓,現(xiàn)在給出定義:

  • 對于短時(shí)間內(nèi)連續(xù)觸發(fā)的事件(上面的滾動事件)寻行,防抖的含義就是讓某個時(shí)間期限(如上面的1000毫秒)內(nèi),事件處理函數(shù)只執(zhí)行一次匾荆。

節(jié)流(throttle)

繼續(xù)思考拌蜘,使用上面的防抖方案來處理問題的結(jié)果是:

  • 如果在限定時(shí)間段內(nèi),不斷觸發(fā)滾動事件(比如某個用戶閑著無聊牙丽,按住滾動不斷的拖來拖去)简卧,只要不停止觸發(fā),理論上就永遠(yuǎn)不會輸出當(dāng)前距離頂部的距離烤芦。

但是如果產(chǎn)品同學(xué)的期望處理方案是:即使用戶不斷拖動滾動條举娩,也能在某個時(shí)間間隔之后給出反饋呢?(此處暫且不論哪種方案更合適,既然產(chǎn)品爸爸說話了我們就先考慮怎么實(shí)現(xiàn))

其實(shí)很簡單:我們可以設(shè)計(jì)一種類似控制閥門一樣定期開放的函數(shù)铜涉,也就是讓函數(shù)執(zhí)行一次后智玻,在某個時(shí)間段內(nèi)暫時(shí)失效,過了這段時(shí)間后再重新激活(類似于技能冷卻時(shí)間)芙代。

效果:如果短時(shí)間內(nèi)大量觸發(fā)同一事件吊奢,那么在函數(shù)執(zhí)行一次之后,該函數(shù)在指定的時(shí)間期限內(nèi)不再工作纹烹,直至過了這段時(shí)間才重新生效页滚。

實(shí)現(xiàn) 這里借助setTimeout來做一個簡單的實(shí)現(xiàn),加上一個狀態(tài)位valid來表示當(dāng)前函數(shù)是否處于工作狀態(tài):

function throttle(fn,delay){
    let valid = true
    return function() {
       if(!valid){
           //休息時(shí)間 暫不接客
           return false 
       }
       // 工作時(shí)間滔韵,執(zhí)行函數(shù)并且在間隔期內(nèi)把狀態(tài)位設(shè)為無效
        valid = false
        setTimeout(() => {
            fn()
            valid = true;
        }, delay)
    }
}
/* 請注意逻谦,節(jié)流函數(shù)并不止上面這種實(shí)現(xiàn)方案,
   例如可以完全不借助setTimeout,可以把狀態(tài)位換成時(shí)間戳陪蜻,然后利用時(shí)間戳差值是否大于指定間隔時(shí)間來做判定邦马。
   也可以直接將setTimeout的返回的標(biāo)記當(dāng)做判斷條件-判斷當(dāng)前定時(shí)器是否存在,如果存在表示還在冷卻宴卖,并且在執(zhí)行fn之后消除定時(shí)器表示激活滋将,原理都一樣
    */

// 以下照舊
function showTop  () {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滾動條位置:' + scrollTop);
}
window.onscroll = throttle(showTop,1000) 

運(yùn)行以上代碼的結(jié)果是:

  • 如果一直拖著滾動條進(jìn)行滾動,那么會以1s的時(shí)間間隔症昏,持續(xù)輸出當(dāng)前位置和頂部的距離

其他應(yīng)用場景舉例

講完了這兩個技巧随闽,下面介紹一下平時(shí)開發(fā)中常遇到的場景:

  1. 搜索框input事件,例如要支持輸入實(shí)時(shí)搜索可以使用節(jié)流方案(間隔一段時(shí)間就必須查詢相關(guān)內(nèi)容)肝谭,或者實(shí)現(xiàn)輸入間隔大于某個值(如500ms)掘宪,就當(dāng)做用戶輸入完成,然后開始搜索攘烛,具體使用哪種方案要看業(yè)務(wù)需求魏滚。
  2. 頁面resize事件,常見于需要做頁面適配的時(shí)候坟漱。需要根據(jù)最終呈現(xiàn)的頁面情況進(jìn)行dom渲染(這種情形一般是使用防抖鼠次,因?yàn)橹恍枰袛嘧詈笠淮蔚淖兓闆r)

思考總結(jié)

上述內(nèi)容基于防抖和節(jié)流的核心思路設(shè)計(jì)了簡單的實(shí)現(xiàn)算法,但是不代表實(shí)際的庫(例如undercore js)的源碼就直接是這樣的芋齿,最起碼的可以看出腥寇,在上述代碼實(shí)現(xiàn)中,因?yàn)?code>showTop本身的很簡單觅捆,無需考慮作用域和參數(shù)傳遞赦役,所以連apply都沒有用到,實(shí)際上肯定還要考慮傳遞argument以及上下文環(huán)境(畢竟apply需要用到this對象)栅炒。這里的相關(guān)知識在本專欄《柯里化》和《this對象》的文章里也有提到扩劝。本文依然堅(jiān)持突出核心代碼庸论,盡可能剝離無關(guān)功能點(diǎn)的思路行文因此不做贅述。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末棒呛,一起剝皮案震驚了整個濱河市聂示,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌簇秒,老刑警劉巖鱼喉,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異趋观,居然都是意外死亡扛禽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進(jìn)店門皱坛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來编曼,“玉大人,你說我怎么就攤上這事剩辟∑。” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵贩猎,是天一觀的道長熊户。 經(jīng)常有香客問我,道長吭服,這世上最難降的妖魔是什么嚷堡? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮艇棕,結(jié)果婚禮上蝌戒,老公的妹妹穿的比我還像新娘。我一直安慰自己沼琉,他們只是感情好北苟,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著刺桃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吸祟。 梳的紋絲不亂的頭發(fā)上瑟慈,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天,我揣著相機(jī)與錄音屋匕,去河邊找鬼葛碧。 笑死,一個胖子當(dāng)著我的面吹牛过吻,可吹牛的內(nèi)容都是我干的进泼。 我是一名探鬼主播蔗衡,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼乳绕!你這毒婦竟也來了绞惦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤洋措,失蹤者是張志新(化名)和其女友劉穎济蝉,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體菠发,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡王滤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了滓鸠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雁乡。...
    茶點(diǎn)故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖糜俗,靈堂內(nèi)的尸體忽然破棺而出踱稍,到底是詐尸還是另有隱情,我是刑警寧澤吩跋,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布寞射,位于F島的核電站,受9級特大地震影響锌钮,放射性物質(zhì)發(fā)生泄漏桥温。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一梁丘、第九天 我趴在偏房一處隱蔽的房頂上張望侵浸。 院中可真熱鬧,春花似錦氛谜、人聲如沸掏觉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽澳腹。三九已至,卻和暖如春杨何,著一層夾襖步出監(jiān)牢的瞬間酱塔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工危虱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留羊娃,地道東北人。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓埃跷,卻偏偏與公主長得像蕊玷,于是被迫代替她去往敵國和親邮利。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評論 2 350