防抖與節(jié)流,不要再分不清啦围苫!

本篇文章主要介紹防抖和節(jié)流的原理裤园,以及它們的區(qū)別。

防抖與節(jié)流的問題總是會在面試中出現(xiàn)(然而我并沒有遇到)剂府,如果你在面試前有背書拧揽,那肯定能過這題的,但如果現(xiàn)實開發(fā)中用的不多的話腺占,估計就很快忘記了怎么寫來著(我就是這樣)淤袜。究其原因就是沒徹底弄清楚這兩個的原理與區(qū)別,所以準備這次來好好梳理一下衰伯。

我們知道前端開發(fā)中會遇到頻繁觸發(fā)的事件饮怯,比如keyup、keydown事件嚎研,mousedown蓖墅、mousemove事件,還有window 的 resize临扮、scroll等论矾。如果任由用戶頻繁觸發(fā)此類事件,將帶來極大的性能消耗杆勇,或可能導致頁面卡頓贪壳。作為有前途的前端人,我們有必要掌握優(yōu)化技巧蚜退,一般解決這類問題的方法也就是防抖節(jié)流了闰靴。

那么問題也就來了,我們可以去網(wǎng)上搜到相關插件钻注,也能搜到很多優(yōu)秀的實現(xiàn)源碼蚂且,那到底什么場景用防抖,什么場景用節(jié)流呢幅恋?我們慢慢來看杏死。

防抖(debounce)

先看防抖,用一句話概括防抖就是:觸發(fā)高頻事件后n秒內(nèi)函數(shù)只會執(zhí)行一次,如果n秒內(nèi)高頻事件再次被觸發(fā)淑翼,則重新計算時間

說的通俗點就是:你盡管頻繁觸發(fā)事件腐巢,但我一定是在觸發(fā)事件的n秒后才執(zhí)行,如果在前一個事件觸發(fā)的n秒內(nèi)又重新觸發(fā)了這個事件玄括,那就以新的事件的時間為準冯丙,n 秒后才執(zhí)行。核心點就是遭京,要等你在觸發(fā)事件后的n秒內(nèi)不再重新觸發(fā)事件银还,我才執(zhí)行

我先放一段基本的防抖函數(shù)源碼:

// fn 函數(shù)傳入用戶方法
// delay 延遲執(zhí)行的時間洁墙,默認 500ms
function debounce(fn, delay = 500) {
  let timer = null
  return function() {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.apply(this, arguments)
      timer = null
    }, delay)
  }
}

社區(qū)有很多文章寫了關于防抖函數(shù)的實現(xiàn)蛹疯,每個人的實現(xiàn)方式可能都有細微區(qū)別,但其核心部分也就是上面這段了热监,對我而言夠用捺弦,后面會詳細說這段代碼。現(xiàn)在我們舉例一個常見場景來應用一下這段代碼孝扛,我們來監(jiān)聽一下 input元素的 keyup事件列吼,每次釋放鍵盤時打印輸入值,我們先不加入防抖

<body>
  <input id="input" type="text" />

  <script>
    let input = document.getElementById('input')

    input.addEventListener('keyup', function() {
      console.log(input.value)
    })
  </script>
</body>

運行上面代碼苦始,可以看到每次釋放鍵盤時控制臺都在打印寞钥,頻率很高,但因為要執(zhí)行的操作只是簡單的打印陌选,所以感受不到性能的消耗理郑。而現(xiàn)實開發(fā)里時常要執(zhí)行的操作是ajax請求,假設 1 秒觸發(fā)了 60 次咨油,每個請求回調(diào)就必須在 1000 / 60 = 16.67ms 內(nèi)完成您炉,否則可能就會出現(xiàn)卡頓。所以優(yōu)化這段操作很有必要役电,我們來應用前面的防抖函數(shù):

  <body>
    <input id="input" type="text" />

    <script>
      let input = document.getElementById('input')

      input.addEventListener(
        'keyup',
        debounce(function() { // 此處通過 debounce 返回用戶操作函數(shù)
          console.log(input.value)
        }, 1000)
      )
    </script>
  </body>

加入防抖的效果大家是可以預見的赚爵,它抑制住了高頻操作,此時用戶持續(xù)輸入法瑟,并在 1s 內(nèi)重新輸入時是不會觸發(fā)打印操作的冀膝,核心就在這個 1s 內(nèi),如果用戶停止輸入并超過 1s ,則會執(zhí)行打印霎挟。我們可以結合上面的debounce函數(shù)源碼來分析一下流程:

  1. 當輸入第一個字符窝剖,并第一次觸發(fā)keyup時,timer為null氓扛,所以開始新的定時任務枯芬,1秒后執(zhí)行打印操作论笔,并晴空timer采郎;
  2. 在 1 秒內(nèi)千所,用戶又輸入了第二個字符,再次觸發(fā)事件蒜埋,此時定時器保存了上一次的任務淫痰,所以執(zhí)行clearTimeout(timer)清空了定時器,并重新賦值新的定時任務整份;
  3. 后續(xù)用戶持續(xù)輸入時待错,反復執(zhí)行上一步的操作;
  4. 當用戶停止輸入時烈评,經(jīng)過 1 秒后火俄,則終于可以執(zhí)行定時器里的任務。

以前不知道為什么這么寫讲冠,現(xiàn)在了解了瓜客,記住就行了。另外debounce函數(shù)中有一個問題一直被人問起竿开,就是為什么要fn.apply(this, arguments)這樣谱仪,而不是直接fn()這樣。其實不使用apply也是可以的否彩,但為了程序的穩(wěn)定性疯攒,還是加入比較好,畢竟又不麻煩列荔。加入apply后解決了兩個不穩(wěn)定因素:

  1. 不使用防抖函數(shù)時敬尺,在fn中打印this,本例中指向的是<input id="input" type="text">贴浙,而在加入防抖函數(shù)后筷转,指向的是Window對象,所以要手動改正 this 指向悬而。
  2. 事件處理函數(shù)中會提供事件對象 event呜舒,使用防抖函數(shù)前后會改變事件對象。比如例子中笨奠,使用防抖前袭蝗,event指向的是 KeyboardEvent對象,加入防抖后則變成 undefined了般婆,所以也要手動傳入?yún)?shù)到腥。

這些都是js基礎,還是需要打牢的蔚袍。如果源碼中的定時器里不是箭頭函數(shù)乡范,就需要這樣寫了:

function debounce(fn, delay = 1000) {
  let timer = null

  return function(...args) {  // 此處顯示定義出參數(shù)對象
    let context = this // 緩存 this 對象

    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(function() { 
      fn.apply(context, args) // 注意改變
      timer = null
    }, delay)
  }
}

節(jié)流(throttle)

也用一句話概括節(jié)流:高頻事件觸發(fā)配名,但在n秒內(nèi)只會執(zhí)行一次,所以節(jié)流會稀釋函數(shù)的執(zhí)行頻率晋辆。

同樣是抑制高頻事件觸發(fā)渠脉,與防抖的區(qū)別在于它不需要用戶停頓,而是在持續(xù)觸發(fā)的過程中每隔 n 秒執(zhí)行一次瓶佳。

實現(xiàn)節(jié)流一般有兩個方向芋膘,一是使用時間戳,而是使用定時器霸饲。

既然是為了比較为朋,那還是使用上面的例子,即監(jiān)聽keyup事件厚脉。我們先看用時間戳來實現(xiàn)節(jié)流

// fn 函數(shù)傳入用戶方法
// wait 間隔執(zhí)行時間习寸,默認 500ms
function throttle(fn, wait=500) {
  // 初始時間點
  let previous = 0
  return function(...args) {
    let context = this
    let now = +new Date() // 當前時間戳
    if (now - previous > wait) {
      fn.apply(context, args)
      previous = now
    }
  }
}

當我們應用這個方法時,即:

input.addEventListener(
  'keyup',
  throttle(function() {
    console.log(input.value)
  }, 1000)  // 便于演示傻工,設定wait為 1秒
)

此時瀏覽器運行代碼霞溪,在input框持續(xù)輸入時,會發(fā)現(xiàn)每隔 1 秒就會打印值精钮。我們來梳理一下流程:

  1. 輸入第一個字符時威鹿,進入節(jié)流邏輯,時間戳肯定大于 1 秒轨香,所以立刻執(zhí)行打印操作忽你,同時將 previous 設定為當前時間戳;
  2. 持續(xù)輸入臂容,間隔時間小于 1 秒時科雳,不執(zhí)行操作,previous不變脓杉,now一直在增長糟秘;
  3. now增長到與previous的差值大于 1000 時,執(zhí)行打印球散,更新previous;
  4. 如此往復尿赚,每隔 1 秒打印一次。而最后輸入的值則不會被打印蕉堰,因為持續(xù)的過程中凌净,最后一次的差值還沒到1000就停止輸入了,超過 1000 時屋讶,則是算重新第一次輸入了冰寻。我說的可能不好明白,自己走一遍流程就清楚了皿渗。

由此斩芭,上面的節(jié)流方案可以做到限制高頻觸發(fā)事件轻腺,它的特點是:使用時間戳方式實現(xiàn)的節(jié)流,在第一次觸發(fā)時會立刻執(zhí)行划乖,而停止觸發(fā)后沒有辦法再執(zhí)行事件贬养。

現(xiàn)在我們再來試試使用定時器實現(xiàn)的節(jié)流方式,放上源碼:

// fn 函數(shù)傳入用戶方法
// wait 間隔執(zhí)行時間迁筛,默認 500ms
function throttle(fn, wait) {
  let timeout
  return function(...args) {
    if (!timeout) {
      timeout = setTimeout(() => {
        fn.apply(this, args)
        timeout = null
      }, wait)
    }
  }
}

使用方法是一樣的:

input.addEventListener(
  'keyup',
  throttle(function() {
    console.log(input.value)
  }, 1000)  // 便于演示煤蚌,設定wait為 1秒
)

此時運行效果依舊是每隔 1 秒執(zhí)行一次耕挨,但也稍有區(qū)別细卧,再來梳理一下這個流程:

  1. 輸入第一個字符時,進入節(jié)流邏輯筒占,初始定時器無值贪庙,所以賦值新的定時任務,1 秒后執(zhí)行翰苫;
  2. 此時用戶在持續(xù)輸入止邮,但因為第 1 步定時器已經(jīng)被賦值了,所以不重新賦值了奏窑,函數(shù)不執(zhí)行邏輯导披;
  3. 此時 1 秒已經(jīng)過去了,第一步中的定時任務觸發(fā)埃唯,執(zhí)行打印操作撩匕,清空定時器;
  4. 繼續(xù)輸入時墨叛,timeout定時器因為被清空了止毕,所以重新賦值,走第 1 步中的邏輯漠趁;
  5. 如此往復扁凛,總是間隔 1 秒執(zhí)行一次〈炒可以發(fā)現(xiàn)第一次觸發(fā)事件時不會立刻執(zhí)行谨朝,而停止輸入時,最后還會執(zhí)行一次甥绿。

自己多過幾遍流程就會很清晰了字币。

總結

來做個總結:

防抖與節(jié)流的區(qū)別

我不想從定義上說區(qū)別,直接從使用結果上比較區(qū)別:

  • 使用防抖:持續(xù)觸發(fā)高頻事件時妹窖,只要觸發(fā)時間間隔小于設定的時間閥值纬朝,不管持續(xù)多久都不會執(zhí)行用戶操作,只有當停頓時間超過設定的時間閥值時骄呼,才會執(zhí)行一次操作共苛。
  • 使用節(jié)流:持續(xù)觸發(fā)高頻事件時判没,每隔一段時間就觸發(fā)一次操作,不需要“停頓”隅茎,這個一段時間是指你設定的時間閥值澄峰。所以在這個持續(xù)的過程中,會多次觸發(fā)操作辟犀,而防抖是一個持續(xù)過程后只觸發(fā)一次俏竞。

所以何時使用防抖,何時使用節(jié)流堂竟,全看你需要的效果魂毁,而效果就是上面總結的。

節(jié)流兩種實現(xiàn)方式的區(qū)別

  • 時間戳方式:事件會立刻執(zhí)行出嘹,事件停止觸發(fā)后沒有辦法再執(zhí)行席楚。
  • 定時器方式:事件會在 n 秒后第一次執(zhí)行,事件停止觸發(fā)后依然會再執(zhí)行一次事件税稼。

當然有時候這兩種節(jié)流方式可能都不能滿足需求烦秩,比如你既想要能夠立即執(zhí)行,也要結束時還能執(zhí)行一次郎仆,又比如你想要自己控制它開始和結束的狀態(tài)只祠,不用怕,社區(qū)里都能找到你要的扰肌,況且我們還有 Lodash 這樣優(yōu)秀的插件抛寝。我在這里只是想要介紹一下他們的原理和區(qū)別。

如有不對之處狡耻,望指正墩剖,謝謝。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末夷狰,一起剝皮案震驚了整個濱河市岭皂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沼头,老刑警劉巖爷绘,帶你破解...
    沈念sama閱讀 210,835評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異进倍,居然都是意外死亡土至,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,900評論 2 383
  • 文/潘曉璐 我一進店門猾昆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來陶因,“玉大人,你說我怎么就攤上這事垂蜗】铮” “怎么了解幽?”我有些...
    開封第一講書人閱讀 156,481評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長烘苹。 經(jīng)常有香客問我躲株,道長,這世上最難降的妖魔是什么镣衡? 我笑而不...
    開封第一講書人閱讀 56,303評論 1 282
  • 正文 為了忘掉前任霜定,我火速辦了婚禮,結果婚禮上廊鸥,老公的妹妹穿的比我還像新娘望浩。我一直安慰自己,他們只是感情好黍图,可當我...
    茶點故事閱讀 65,375評論 5 384
  • 文/花漫 我一把揭開白布曾雕。 她就那樣靜靜地躺著奴烙,像睡著了一般助被。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上切诀,一...
    開封第一講書人閱讀 49,729評論 1 289
  • 那天揩环,我揣著相機與錄音,去河邊找鬼幅虑。 笑死丰滑,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的倒庵。 我是一名探鬼主播褒墨,決...
    沈念sama閱讀 38,877評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼擎宝!你這毒婦竟也來了郁妈?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,633評論 0 266
  • 序言:老撾萬榮一對情侶失蹤绍申,失蹤者是張志新(化名)和其女友劉穎噩咪,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體极阅,經(jīng)...
    沈念sama閱讀 44,088評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡胃碾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,443評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了筋搏。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仆百。...
    茶點故事閱讀 38,563評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖奔脐,靈堂內(nèi)的尸體忽然破棺而出俄周,到底是詐尸還是另有隱情栏账,我是刑警寧澤,帶...
    沈念sama閱讀 34,251評論 4 328
  • 正文 年R本政府宣布栈源,位于F島的核電站挡爵,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏甚垦。R本人自食惡果不足惜茶鹃,卻給世界環(huán)境...
    茶點故事閱讀 39,827評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望艰亮。 院中可真熱鬧闭翩,春花似錦、人聲如沸迄埃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,712評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽侄非。三九已至蕉汪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間逞怨,已是汗流浹背者疤。 一陣腳步聲響...
    開封第一講書人閱讀 31,943評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留叠赦,地道東北人驹马。 一個月前我還...
    沈念sama閱讀 46,240評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像除秀,于是被迫代替她去往敵國和親糯累。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,435評論 2 348