JavaScript之函數(shù)防抖、節(jié)流

配圖源自 Freepik

一罩润、前言

相信無論在實(shí)際應(yīng)用場(chǎng)景玖翅、亦或是面試,都會(huì)經(jīng)常遇得到函數(shù)防抖割以、函數(shù)節(jié)流等金度,下面我們來聊一聊吧。

先放出一個(gè)示例:

import React, { useEffect, useRef } from 'react'
import debounce from '../../utils/debounce'
import throttle from '../../utils/throttle'
import style from './index.scss'

export default function Demo(props) {
  const inputElem1 = useRef()
  const inputElem2 = useRef()
  const inputElem3 = useRef()

  useEffect(() => {
    inputElem1.current.addEventListener('keyup', request)
    inputElem2.current.addEventListener('keyup', debounce(request, 1000))
    inputElem3.current.addEventListener('keyup', throttle(request, 3000))
  }, [])

  function request(event) {
    const { value } = event.target
    console.log(`Http request: ${value}.`)
  }

  return (
    <div className={style.container}>
      <div className={style.list}>
        <label htmlFor="input1">普通輸入框:</label>
        <input name="input1" ref={inputElem1} defaultValue="" />
      </div>

      <div className={style.list}>
        <label htmlFor="input2">防抖輸入框:</label>
        <input name="input2" ref={inputElem2} defaultValue="" />
      </div>

      <div className={style.list}>
        <label htmlFor="input3">節(jié)流輸入框:</label>
        <input name="input3" ref={inputElem3} defaultValue="" />
      </div>
    </div>
  )
}

以上 Demo 只有三個(gè)輸入框严沥,很簡單猜极。我給每個(gè)輸入框綁定了一個(gè) keyup 鍵盤事件,該事件執(zhí)行會(huì)發(fā)起網(wǎng)絡(luò)請(qǐng)求(為了更簡潔消玄,這里只是打印一下而已)跟伏,而對(duì)應(yīng)防抖、節(jié)流輸入框則經(jīng)過相應(yīng)的處理翩瓜。

二受扳、函數(shù)防抖(debounce)

如果我們?cè)?strong>普通輸入框快速鍵入 12345,可以從控制臺(tái)上的打印結(jié)果看到兔跌,它會(huì)發(fā)起 5 次網(wǎng)絡(luò)請(qǐng)求(假設(shè)我們這個(gè)是一個(gè)簡單的搜索引擎)勘高。

還不知道用什么截屏/錄屏軟件可以生成 GIF 動(dòng)圖,有時(shí)間再研究下...

從實(shí)際場(chǎng)景考慮,如果每鍵入一個(gè)字符就立刻發(fā)起網(wǎng)絡(luò)請(qǐng)求华望,去檢索結(jié)果蕊蝗,這是非常影響體驗(yàn)的。假設(shè)我們限制為:用戶在停止輸入后 1s 后才發(fā)起網(wǎng)絡(luò)請(qǐng)求立美。

要實(shí)現(xiàn)這樣的需求匿又,我們只有使用函數(shù)防抖即可。

2.1 什么是函數(shù)防抖建蹄?

概念:在一定時(shí)間間隔內(nèi)碌更,事件處理函數(shù)只會(huì)執(zhí)行一次。若在該時(shí)間間隔內(nèi)(多次)重新觸發(fā)洞慎,則重新計(jì)時(shí)痛单。

怎么理解?

  • 假設(shè)用戶鍵入字母 a 后就停止輸入了劲腿,那么網(wǎng)絡(luò)請(qǐng)求會(huì)在停止鍵入操作的 1s 后發(fā)起旭绒。這個(gè)很好理解。
  • 若用戶繼續(xù)鍵入字母 b 后焦人,若有所思地停了一會(huì)(這個(gè)時(shí)間在 1s 之內(nèi)挥吵,假設(shè)為 800ms 吧),接著鍵入字母 c花椭,之后就停止鍵入了忽匈。網(wǎng)絡(luò)請(qǐng)求會(huì)發(fā)生在鍵入字母 c 的 1s 后被發(fā)起,而不是鍵入字母 b 之后的 1s 發(fā)起矿辽。因?yàn)楹瘮?shù)防抖會(huì)在鍵入 c 之后重新計(jì)時(shí)丹允。
2.2 函數(shù)防抖實(shí)現(xiàn)

debounce(func, wait)

實(shí)現(xiàn)思路:

首先,接收兩個(gè)參數(shù) func(要防抖的函數(shù)袋倔,一般是事件回調(diào)函數(shù))和 wait(需要延遲的時(shí)間間隔雕蔽,單位毫秒)。然后 funcsetTimeout 中執(zhí)行宾娜,而 setTimeout 的延遲時(shí)間就是 wait批狐。而重新計(jì)時(shí)的話,則在每次觸發(fā)的時(shí)候 clearTimeout 即可實(shí)現(xiàn)碳默。

需要注意下贾陷,func 的執(zhí)行上下文(this)及其入?yún)ⅰ?/p>

// debounce.js
function debounce(func, wait) {
  let timerId
  return function () {
    // 當(dāng)前運(yùn)行上下文環(huán)境,以及實(shí)參
    const context = this
    const args = arguments

    // 重新計(jì)時(shí)(關(guān)鍵是這一步)
    // 在 wait 時(shí)間內(nèi)嘱根,若重新觸發(fā)髓废,清除 clearTiemout,以達(dá)到重新計(jì)時(shí)的效果
    if(timerId) clearTimeout(timerId)

    timerId = setTimeout(function () {
      // 綁定上下文和參數(shù)该抒,否則實(shí)參 func 的 this 指向 window 對(duì)象慌洪,參數(shù)為空
      func.apply(context, args)
    }, wait)
  }
}

借助 ES6 的 Rest 參數(shù)箭頭函數(shù)語法,簡化一下:

function debounce(func, wait) {
  let timerId
  return function (...args) {
    if (timerId) clearTimeout(timerId)
    timerId = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}

依次在對(duì)應(yīng)輸入框內(nèi)鍵入 12345,對(duì)比下防抖前后的結(jié)果:

兩次鍵入速度差不多冈爹,而且每個(gè)字符鍵入時(shí)間間隔小于 1s(可調(diào)大延遲執(zhí)行時(shí)間涌攻,更容易對(duì)比)。

// 普通輸入框
inputElem1.current.addEventListener('keyup', request)
// 防抖輸入框
inputElem2.current.addEventListener('keyup', debounce(request, 1000))
無防抖處理
防抖處理

對(duì)比以上無防抖處理和防抖處理的結(jié)果频伤,可以看到前者每鍵入一個(gè)字符都會(huì)執(zhí)行回調(diào)函數(shù)恳谎,而后者則會(huì)在最后一次觸發(fā)的 N 毫秒(即 wait 延遲時(shí)間)之后才會(huì)執(zhí)行一次回調(diào)函數(shù)。

還有一種是“立即執(zhí)行”的函數(shù)防抖:區(qū)別在于第一次觸發(fā)時(shí)憋肖,是否立即執(zhí)行回調(diào)函數(shù)因痛。

再結(jié)合以上的“非立即執(zhí)行”的防抖,完整方法如下:

/**
 * 函數(shù)防抖
 * @param {Function} func 要防抖的函數(shù)
 * @param {number} wait 需要延遲的毫秒數(shù)
 * @param {boolean} immediate 是否立即執(zhí)行
 * @returns {Function} 返回新的 debounced(防抖動(dòng))函數(shù)
 */
function debounce(func, wait = 0, immediate = false) {
  let timerId
  return function (...args) {
    if (timerId) clearTimeout(timerId)

    if (immediate && !timerId) {
      func.apply(this, args)
    }

    timerId = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}

當(dāng)我們修改成:

inputElem2.current.addEventListener('keyup', debounce(request, 1000, true))

從以下結(jié)果可以看到岸更,當(dāng)我在防抖輸入框鍵入 12345 的時(shí)候鸵膏,它會(huì)在鍵入 1 時(shí)立刻發(fā)起一次網(wǎng)絡(luò)請(qǐng)求,由于每個(gè)字符鍵入的時(shí)間間隔都在 1s 之內(nèi)怎炊,因此它只會(huì)在最后停止鍵入的 1s 后才會(huì)發(fā)起網(wǎng)絡(luò)請(qǐng)求谭企。

三、函數(shù)節(jié)流(throttle)

概念:在一定時(shí)間間隔內(nèi)只會(huì)觸發(fā)一次函數(shù)评肆。若在該時(shí)間間隔內(nèi)觸發(fā)多次函數(shù)债查,只有第一次生效。

3.1 函數(shù)節(jié)流實(shí)現(xiàn)
function throttle(func, wait) {
  // 記錄上一次執(zhí)行 func 的時(shí)間
  let prev = 0
  return function (...args) {
    // 當(dāng)前觸發(fā)的時(shí)間(時(shí)間戳)
    const now = Number(new Date()) // +new Date()
    
    // 單位時(shí)間內(nèi)只會(huì)執(zhí)行一次
    if (now >= prev + wait) {
      // 符合條件執(zhí)行 func 時(shí)瓜挽,需要更新 prev 時(shí)間
      prev = now
      func.apply(this, args)
    }
  }
}
3.2 函數(shù)節(jié)流優(yōu)化

以上節(jié)流方法有個(gè)問題攀操,假設(shè)節(jié)流控制間隔時(shí)間為 1s,若最后一次觸發(fā)時(shí)間在 1.5s秸抚,則最后一次觸發(fā)并不會(huì)執(zhí)行。因此歹垫,需要在節(jié)流中嵌入防抖思想剥汤,以保證最后一次會(huì)被觸發(fā)。

function throttle(func, wait) {
  // 記錄上一次執(zhí)行 func 的時(shí)間
  let prev = 0
  let timerId
  return function (...args) {
    // 當(dāng)前觸發(fā)的時(shí)間(時(shí)間戳)
    const now = Number(new Date()) // +new Date()

    // 保證最后一次也會(huì)觸發(fā)
    // 我看到很多文章排惨,將清除定時(shí)器的步驟放到 2?? 里面
    // 我認(rèn)為應(yīng)該放在這里才對(duì)吭敢,原因看我下面舉例的場(chǎng)景。
    if (timerId) clearTimeout(timerId)
    
    if (now >= prev + wait) {
      // 1??
      // 符合條件執(zhí)行 func 時(shí)暮芭,需要更新 prev 時(shí)間
      prev = now
      func.apply(this, args)
    } else {
      // 2??
      // 單位時(shí)間內(nèi)只會(huì)執(zhí)行一次
      // if (timerId) clearTimeout(timerId) // 不應(yīng)該放在這里
      timerId = setTimeout(() => {
        prev = now
        func.apply(this, args)
      }, wait)
    }
  }
}

假設(shè)我將 clearTimeout() 放在了 2?? 里面鹿驼,而不是在外層≡辏基于 throttle(func, 1000) 考慮以下場(chǎng)景:

我在 4s 時(shí)觸發(fā)了一次畜晰,應(yīng)該走 1?? 邏輯。然后在 4.9s 時(shí)又觸發(fā)了一次瑞筐,這會(huì)走的 2?? 邏輯并記錄了一個(gè)定時(shí)任務(wù)凄鼻。然后時(shí)間到了 5s,我又觸發(fā)了一次(后面就停止操作了),它會(huì)走 1?? 邏輯一次块蚌,接著時(shí)間來的 5.9s闰非,它還會(huì)執(zhí)行一遍 fn.apply(this, args),因?yàn)樵?5s 觸發(fā)時(shí)峭范,沒有 clearTimeout()财松。因此,清除定時(shí)器的步驟應(yīng)該放在外層纱控,以保證每次被觸發(fā)是都清掉最后一次的定時(shí)器辆毡,避免在一些邊界 Case 觸發(fā)兩次。

當(dāng)然其徙,以上場(chǎng)景是在理想的狀態(tài)胚迫,實(shí)際場(chǎng)景可能幾乎碰不到這些邊界。但從嚴(yán)謹(jǐn)?shù)慕嵌热タ磫栴}唾那,應(yīng)該也要考慮的访锻。

寫到這里,我又在想剛剛的“立即執(zhí)行的函數(shù)防抖”闹获,跟這個(gè)優(yōu)化版的節(jié)流是不是有點(diǎn)像期犬,第一次觸發(fā)都會(huì)執(zhí)行回調(diào)函數(shù)。但區(qū)別是防抖會(huì)重新計(jì)時(shí)避诽,而節(jié)流在第一次觸發(fā)后面的每個(gè)間隔時(shí)間點(diǎn)都會(huì)觸發(fā)龟虎,非間隔點(diǎn)的最后一次觸發(fā)也將會(huì)被執(zhí)行。

我在節(jié)流輸入框內(nèi)沙庐,依次鍵入 1234567890鲤妥,可以看到:在鍵入字符 1 時(shí)執(zhí)行了回調(diào);接著鍵入的 234拱雏、67 字符都屬在上一個(gè)時(shí)間間隔內(nèi)棉安,因此無法執(zhí)行回調(diào)。其中鍵入的 90 字符應(yīng)屬于 8 之后的 1s 周期之內(nèi)铸抑,由于鍵入 0 字符屬于最后一次的非時(shí)間間隔內(nèi)的觸發(fā)動(dòng)作贡耽,因此回調(diào)會(huì)在鍵入 0 的 1s 后被執(zhí)行刀森。(可打印時(shí)間戳的形式磨淌,更精細(xì)地對(duì)比)

inputElem3.current.addEventListener('keyup', throttle(request, 1000))
節(jié)流處理

四头滔、防抖與節(jié)流

其實(shí)饰恕,函數(shù)防抖和函數(shù)節(jié)流都是為了防止某個(gè)時(shí)間段頻繁觸發(fā)某個(gè)事件抖剿。它倆在某個(gè)時(shí)間間隔內(nèi)多次重復(fù)觸發(fā)欺抗,都只會(huì)執(zhí)行一次回調(diào)函數(shù)县耽。區(qū)別在于函數(shù)防抖最后一次觸發(fā)有效吓揪,而函數(shù)節(jié)流則是第一次觸發(fā)有效至耻。

而在上面氏涩,都對(duì)函數(shù)防抖和函數(shù)節(jié)流做了“拓展”届囚,例如:

  • 在函數(shù)防抖中,增加了 immediate 的參數(shù)是尖,用于控制第一次是否執(zhí)行回調(diào)意系。
  • 在函數(shù)節(jié)流中,允許最后一次在非時(shí)間間隔的觸發(fā)動(dòng)作有效饺汹。

應(yīng)用場(chǎng)景:

  • 函數(shù)防抖(debounce)

    • 搜索場(chǎng)景:防止用戶不停地輸入蛔添,來節(jié)約請(qǐng)求資源。
    • window resize:調(diào)整瀏覽器窗口大小時(shí)兜辞,利用防抖使其只觸發(fā)一次迎瞧。
  • 函數(shù)節(jié)流(throttle)

    • 鼠標(biāo)事件、mousemove 拖拽
    • 監(jiān)聽滾動(dòng)事件

如果還是不太明白 debounce 和 throttle 的差異逸吵,可以在以下這個(gè)頁面凶硅,可視化體驗(yàn)。

五扫皱、拓展

還是那句話:

生產(chǎn)環(huán)境請(qǐng)使用 Lodash 庫足绅,對(duì)應(yīng)的方法是 _.debounce()_.throttle()

畢竟 Lodash 是經(jīng)過社區(qū)考驗(yàn)的韩脑,肯定會(huì)完善很多氢妈。而我這篇文章可能會(huì)有一些我未曾想到的場(chǎng)景沒有處理的,面向?qū)W習(xí)和面試(手動(dòng)狗頭)段多。

如有不足首量,歡迎指出 ?? ~

TODO List:

六进苍、參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末加缘,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子觉啊,更是在濱河造成了極大的恐慌生百,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柄延,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡缀程,警方通過查閱死者的電腦和手機(jī)搜吧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杨凑,“玉大人滤奈,你說我怎么就攤上這事×寐” “怎么了蜒程?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵绅你,是天一觀的道長。 經(jīng)常有香客問我昭躺,道長忌锯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任领炫,我火速辦了婚禮偶垮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘帝洪。我一直安慰自己似舵,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布葱峡。 她就那樣靜靜地躺著砚哗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪砰奕。 梳的紋絲不亂的頭發(fā)上蛛芥,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音脆淹,去河邊找鬼常空。 笑死,一個(gè)胖子當(dāng)著我的面吹牛盖溺,可吹牛的內(nèi)容都是我干的漓糙。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼烘嘱,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼昆禽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蝇庭,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤醉鳖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后哮内,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盗棵,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年北发,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纹因。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡琳拨,死狀恐怖瞭恰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情狱庇,我是刑警寧澤惊畏,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布恶耽,位于F島的核電站,受9級(jí)特大地震影響颜启,放射性物質(zhì)發(fā)生泄漏偷俭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一农曲、第九天 我趴在偏房一處隱蔽的房頂上張望社搅。 院中可真熱鬧,春花似錦乳规、人聲如沸形葬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽笙以。三九已至,卻和暖如春冻辩,著一層夾襖步出監(jiān)牢的瞬間猖腕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國打工恨闪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留倘感,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓咙咽,卻偏偏與公主長得像老玛,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子钧敞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 背景 當(dāng)我們進(jìn)行窗口resize蜡豹、scroll、input框內(nèi)容校驗(yàn)等操作時(shí)溉苛,如果事件函數(shù)調(diào)用頻率不加控制镜廉。會(huì)加重...
    夏末遠(yuǎn)歌閱讀 309評(píng)論 0 0
  • 一.認(rèn)識(shí)防抖和節(jié)流 JavaScript是事件驅(qū)動(dòng)的,大量的操作會(huì)觸發(fā)事件愚战,加入到事件隊(duì)列中處理娇唯。 對(duì)于某些頻繁的...
    matexia閱讀 142評(píng)論 0 1
  • 概述 函數(shù)防抖是指將多次觸發(fā)合并成一次執(zhí)行,一般情況下都是合并到最后一次觸發(fā)執(zhí)行寂玲。函數(shù)節(jié)流是指在一段時(shí)間內(nèi)執(zhí)行一次...
    jaimor閱讀 755評(píng)論 0 7
  • 在前端開發(fā)中塔插,經(jīng)常會(huì)給元素添加一些事件,例如:click敢茁、scroll、input留美、mousemove彰檬。 這些事件...
    前端很忙閱讀 369評(píng)論 0 3
  • 表情是什么伸刃,我認(rèn)為表情就是表現(xiàn)出來的情緒。表情可以傳達(dá)很多信息逢倍。高興了當(dāng)然就笑了捧颅,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 125,093評(píng)論 2 7