JavaScript防抖節(jié)流原理

原文鏈接 http://blog.poetries.top/2018/12/21/js-debounce/

一都许、防抖debounce

你是否在日常開發(fā)中遇到一個問題命辖,在滾動事件中需要做個復雜計算或者實現(xiàn)一個按鈕的防二次點擊操作

  • 這些需求都可以通過函數(shù)防抖動來實現(xiàn)。如果在頻繁的事件回調(diào)中做復雜計算恤批,很有可能導致頁面卡頓异吻,不如將多次計算合并為一次計算,只在一個精確點做操作
  • 防抖和節(jié)流的作用都是防止函數(shù)多次調(diào)用。區(qū)別在于诀浪,假設一個用戶一直觸發(fā)這個函數(shù)棋返,且每次觸發(fā)函數(shù)的間隔小于wait,防抖的情況下只會調(diào)用一次雷猪,而節(jié)流的 情況會每隔一定時間(參數(shù)wait)調(diào)用函數(shù)

持續(xù)觸發(fā)scroll事件時睛竣,并不執(zhí)行handle函數(shù),當1000毫秒內(nèi)沒有觸發(fā)scroll事件時求摇,才會延時觸發(fā)scroll事件

image.png
// 防抖
function debounce(fn, wait) {    
    var timeout = null;    
    return function() {        
        if(timeout !== null)   clearTimeout(timeout);        
        timeout = setTimeout(fn, wait);    
    }
}
// 處理函數(shù)
function handle() {    
    console.log(Math.random()); 
}
// 滾動事件
// 當持續(xù)觸發(fā)scroll事件時射沟,事件處理函數(shù)handle只在停止?jié)L動1000毫秒之后才會調(diào)用一次,也就是說在持續(xù)觸發(fā)scroll事件的過程中与境,事件處理函數(shù)handle一直沒有執(zhí)行
window.addEventListener('scroll', debounce(handle, 1000));

我們先來看一個袖珍版的防抖理解一下防抖的實現(xiàn)

// func是用戶傳入需要防抖的函數(shù)
// wait是等待時間
const debounce = (func, wait = 50) => {
  // 緩存一個定時器id
  let timer = 0
  // 這里返回的函數(shù)是每次用戶實際調(diào)用的防抖函數(shù)
  // 如果已經(jīng)設定過定時器了就清空上一次的定時器
  // 開始一個新的定時器验夯,延遲執(zhí)行用戶傳入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 不難看出如果用戶調(diào)用該函數(shù)的間隔小于wait的情況下,上一次的時間還未到就被清除了摔刁,并不會執(zhí)行函數(shù)

這是一個簡單版的防抖挥转,但是有缺陷,這個防抖只能在最后調(diào)用共屈。一般的防抖會有immediate選項绑谣,表示是否立即調(diào)用。這兩者的區(qū)別趁俊,舉個栗子來說

  • 例如在搜索引擎搜索問題的時候域仇,我們當然是希望用戶輸入完最后一個字才調(diào)用查詢接口刑然,這個時候適用延遲執(zhí)行的防抖函數(shù)寺擂,它總是在一連串(間隔小于wait的)函數(shù)觸發(fā)之后調(diào)用。
  • 例如用戶給interviewMapstar的時候泼掠,我們希望用戶點第一下的時候就去調(diào)用接口怔软,并且成功之后改變star按鈕的樣子,用戶就可以立馬得到反饋是否star成功了择镇,這個情況適用立即執(zhí)行的防抖函數(shù)挡逼,它總是在第一次調(diào)用,并且下一次調(diào)用必須與前一次調(diào)用的時間間隔大于wait才會觸發(fā)

完整代碼

下面我們來實現(xiàn)一個帶有立即執(zhí)行選項的防抖函數(shù)

// 這個是用來獲取當前時間戳的
function now() {
  return +new Date()
}
/**
 * 防抖函數(shù)腻豌,返回函數(shù)連續(xù)調(diào)用時家坎,空閑時間必須大于或等于 wait,func 才會執(zhí)行
 *
 * @param  {function} func        回調(diào)函數(shù)
 * @param  {number}   wait        表示時間窗口的間隔
 * @param  {boolean}  immediate   設置為ture時吝梅,是否立即調(diào)用函數(shù)
 * @return {function}             返回客戶調(diào)用函數(shù)
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args

  // 延遲執(zhí)行函數(shù)
  const later = () => setTimeout(() => {
    // 延遲函數(shù)執(zhí)行完畢虱疏,清空緩存的定時器序號
    timer = null
    // 延遲執(zhí)行的情況下,函數(shù)會在延遲函數(shù)中執(zhí)行
    // 使用到之前緩存的參數(shù)和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 這里返回的函數(shù)是每次實際調(diào)用的函數(shù)
  return function(...params) {
    // 如果沒有創(chuàng)建延遲執(zhí)行函數(shù)(later)苏携,就創(chuàng)建一個
    if (!timer) {
      timer = later()
      // 如果是立即執(zhí)行做瞪,調(diào)用函數(shù)
      // 否則緩存參數(shù)和調(diào)用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 如果已有延遲執(zhí)行函數(shù)(later),調(diào)用的時候清除原來的并重新設定一個
    // 這樣做延遲函數(shù)會重新計時
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}
  • 對于按鈕防點擊來說的實現(xiàn):如果函數(shù)是立即執(zhí)行的,就立即調(diào)用装蓬,如果函數(shù)是延遲執(zhí)行的著拭,就緩存上下文和參數(shù),放到延遲函數(shù)中去執(zhí)行牍帚。一旦我開始一個定時器儡遮,只要我定時器還在,你每次點擊我都重新計時暗赶。一旦你點累了峦萎,定時器時間到,定時器重置為 null忆首,就可以再次點擊了爱榔。
  • 對于延時執(zhí)行函數(shù)來說的實現(xiàn):清除定時器ID,如果是延遲調(diào)用就調(diào)用函數(shù)

二糙及、節(jié)流throttle

防抖動和節(jié)流本質(zhì)是不一樣的详幽。防抖動是將多次執(zhí)行變?yōu)樽詈笠淮螆?zhí)行,節(jié)流是將多次執(zhí)行變成每隔一段時間執(zhí)行

如下圖浸锨,持續(xù)觸發(fā)scroll事件時唇聘,并不立即執(zhí)行handle函數(shù),每隔1000毫秒才會執(zhí)行一次handle函數(shù)

image.png

節(jié)流版本

/**
 * underscore 節(jié)流函數(shù)柱搜,返回函數(shù)連續(xù)調(diào)用時迟郎,func 執(zhí)行頻率限定為 次 / wait
 *
 * @param  {function}   func      回調(diào)函數(shù)
 * @param  {number}     wait      表示時間窗口的間隔
 * @param  {object}     options   如果想忽略開始函數(shù)的的調(diào)用,傳入{leading: false}聪蘸。
 *                                如果想忽略結尾函數(shù)的調(diào)用宪肖,傳入{trailing: false}
 *                                兩者不能共存,否則函數(shù)不能執(zhí)行
 * @return {function}             返回客戶調(diào)用函數(shù)   
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的時間戳
    var previous = 0;
    // 如果 options 沒傳則設為空對象
    if (!options) options = {};
    // 定時器回調(diào)函數(shù)
    var later = function() {
      // 如果設置了 leading健爬,就將 previous 設為 0
      // 用于下面函數(shù)的第一個 if 判斷
      previous = options.leading === false ? 0 : _.now();
      // 置空一是為了防止內(nèi)存泄漏控乾,二是為了下面的定時器判斷
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 獲得當前時間戳
      var now = _.now();
      // 首次進入前者肯定為 true
      // 如果需要第一次不執(zhí)行函數(shù)
      // 就將上次時間戳設為當前的
      // 這樣在接下來計算 remaining 的值時會大于0
      if (!previous && options.leading === false) previous = now;
      // 計算剩余時間
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果當前調(diào)用已經(jīng)大于上次調(diào)用時間 + wait
      // 或者用戶手動調(diào)了時間
      // 如果設置了 trailing,只會進入這個條件
      // 如果沒有設置 leading娜遵,那么第一次會進入這個條件
      // 還有一點蜕衡,你可能會覺得開啟了定時器那么應該不會進入這個 if 條件了
      // 其實還是會進入的,因為定時器的延時
      // 并不是準確的時間设拟,很可能你設置了2秒
      // 但是他需要2.2秒才觸發(fā)慨仿,這時候就會進入這個條件
      if (remaining <= 0 || remaining > wait) {
        // 如果存在定時器就清理掉否則會調(diào)用二次回調(diào)
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判斷是否設置了定時器和 trailing
        // 沒有的話就開啟一個定時器
        // 并且不能不能同時設置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
};
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市纳胧,隨后出現(xiàn)的幾起案子镰吆,更是在濱河造成了極大的恐慌,老刑警劉巖躲雅,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鼎姊,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機相寇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門慰于,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人唤衫,你說我怎么就攤上這事婆赠。” “怎么了佳励?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵休里,是天一觀的道長。 經(jīng)常有香客問我赃承,道長妙黍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任瞧剖,我火速辦了婚禮拭嫁,結果婚禮上,老公的妹妹穿的比我還像新娘抓于。我一直安慰自己做粤,他們只是感情好,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布捉撮。 她就那樣靜靜地躺著怕品,像睡著了一般。 火紅的嫁衣襯著肌膚如雪巾遭。 梳的紋絲不亂的頭發(fā)上肉康,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機與錄音恢总,去河邊找鬼迎罗。 笑死,一個胖子當著我的面吹牛片仿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播尤辱,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼砂豌,長吁一口氣:“原來是場噩夢啊……” “哼粤攒!你這毒婦竟也來了屡贺?” 一聲冷哼從身側響起唉窃,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤弦叶,失蹤者是張志新(化名)和其女友劉穎婉宰,沒想到半個月后斤彼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸦泳,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡岔乔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了咖熟。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片圃酵。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖馍管,靈堂內(nèi)的尸體忽然破棺而出郭赐,到底是詐尸還是另有隱情,我是刑警寧澤确沸,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布捌锭,位于F島的核電站,受9級特大地震影響罗捎,放射性物質(zhì)發(fā)生泄漏观谦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一桨菜、第九天 我趴在偏房一處隱蔽的房頂上張望坎匿。 院中可真熱鬧,春花似錦雷激、人聲如沸替蔬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽承桥。三九已至,卻和暖如春根悼,著一層夾襖步出監(jiān)牢的瞬間凶异,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工挤巡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留剩彬,地道東北人。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓矿卑,卻偏偏與公主長得像喉恋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子母廷,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

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