[深刻10] Debounce Throttle

導航

[深刻01] 執行上下文
[深刻02] 原型鏈
[深刻03] 繼承
[深刻04] 事件循環
[深刻05] 柯里化 偏函數 函數記憶
[深刻06] 隱式轉換 和 運算符
[深刻07] 瀏覽器緩存機制(http緩存機制)
[深刻08] 前端安全
[深刻09] 深淺拷貝
[深刻10] Debounce Throttle
[深刻11] 前端路由
[深刻12] 前端模塊化
[深刻13] 觀察者模式 發佈訂閱模式 雙向數據綁定
[深刻14] canvas
[深刻15] webSocket
[深刻16] webpack
[深刻17] http 和 https
[深刻18] CSS-interview
[react] Hookshtml

[部署01] Nginx
[部署02] Docker 部署vue項目
[部署03] gitlab-CI前端

[源碼-webpack01-前置知識] AST抽象語法樹
[源碼-webpack02-前置知識] Tapable
[源碼-webpack03] 手寫webpack - compiler簡單編譯流程vue

Debounce 防抖函數

  • 特色:延時執行,若是在延時的時間內屢次觸發,則重新計時
  • 過程:當事件A發生時,設置一個定時器,a秒後觸發A的回調函數,若是在a秒內有新的同一事件發生,則清除定時器,並重新開始計時(即又在a秒後觸發A的回調,注意:上次的A的回調並未觸發,而是定時器被清除了,定時器中A的回調就不會被執行)

版本一 (基礎版本)

  • 優勢:能夠傳參,好比點擊時,點擊事件提供的 event 對象
  • 缺點:
    • 第一次觸發是不須要延時的,版本一的第一次也是須要定時器的delay時間後纔會執行
    • 不能手動取消debounce的執行,在delay時間未到時的最後一次的執行
版本一 (基礎版本)

/**
 * @param {function} fn 須要debounce防抖函數處理的函數
 * @param {number} delay 定時器延時的時間
 */
function debounce(fn, delay) {
    let timer = null 
    // 該變量常駐內存,能夠記住上一次的狀態
    // 只有在外層函數失去引用時,該變量纔會清除
    // 緩存定時器id
    
    return (...args) => {
      // 返回一個閉包
      // 注意參數:好比事件對象 event 可以獲取到
      if (timer) {
        // timer存在,就清除定時器
        // 清除定時器,則定時器對應的回調函數也就不會執行
        clearTimeout(timer)
      }
      // 清除定時器後,從新計時
      timer = setTimeout(() => {
        fn.call(this, ...args)
        // this須要定時器回調函數時才能肯定,this指向調用時所在的對象,大多數狀況都指向window
      }, delay)
    }
  }
複製代碼

版本二 (升級版本)

  • 解決問題:解決第一次點擊不能當即觸發的問題
  • 解決問題:在delay時間沒有到時,手動的取消debounce的執行
  • 實現的結果:
    • 第一次點擊當即觸發
    • 若是從第一次點擊開始,一直不間斷頻繁點擊(未超過delay時間),而後中止點擊再也不點擊,會觸發兩次,第一次是當即執行的,第二次是debounce延時執行的
    • 能夠手動取消debounce的執行 其實就是手動清除最後一次的timer
版本二 (升級版本)

/**
 * @param {function} fn 須要debounce防抖函數處理的函數
 * @param {number} delay 定時器延時的時間
 * @param {boolean} immediate 是否當即執行
 */
function debounce(fn, delay, immediate) {
    let timer = null
    return (...args) => { // 這裏能夠拿到事件對象
      if (immediate && !timer) {
        // 若是當即執行標誌位是 true,而且timer不存在
        // 即第一次觸發的狀況
        // 之後的觸發因爲timer存在,則再也不進入執行
        // 注意:timer是setTimeout()執行返回的值,不是setTimeout()的回調執行時才返回,是當即返回的
        // 注意:因此第二次觸發時,timer就已經有值了,不是setTimeout()的回調執行時才返回
        fn.call(this, ...args)
      }
      if (timer) {
        clearTimeout(timer)
        // timer存在,就清除定時器
        // 清除定時器,則定時器對應的回調函數也就不會執行
      }
      timer = setTimeout(() => {
        console.log(args, 'args')
        console.log(this, 'this')
        fn.call(this, ...args)
        // 注意:有一個特殊狀況
        // 好比:只點擊一次,在上面的immediate&&!timer判斷中會當即執行一次,而後在delay後,定時器中也會觸發一次
        
        // --------------------
        // if (!immediate) {
        //  fn.call(this, ...args)
        // }
        // immediate = false
        // 註釋的操做能夠只在點擊一次沒有再點擊的狀況只執行一次
        // 可是:一次性屢次點擊,第二次不會觸發,只有再停頓達到delay後,再次點擊纔會正常的達到debounce的效果
         // --------------------
        
      }, delay)
      
      // 手動取消執行debounce函數
      debounce.cancel = function () {
        clearTimeout(timer)
      }
    }
  }
複製代碼

版本三 (變動需求)

  • 需求:第一次當即執行,而後等到中止觸發delay毫秒後,才能夠從新觸發
版本三 (變動需求)
需求:第一次當即執行,而後等到中止觸發delay毫秒後,才能夠從新觸發

/**
 * @param {function} fn 須要debounce防抖函數處理的函數
 * @param {number} delay 定時器延時的時間
 * @param {boolean} immediate 是否當即執行
 */
function debounce(fn, delay, immediate) {
  let timer
  return (...args) => {

    if (timer) {
      clearTimeout(timer)
    }

    if(!immediate) {
      // 不當即執行的狀況
      // 和最初的版本同樣
      timer = setTimeout(() => {
        fn.call(this, ...args)
      }, delay)
    } else {
      // 當即執行
      const cacheTimer = timer // 緩存timer
      // 緩存timer, 由於下面timer會當即改變,若是直接用timer判斷,fn不會執行
      // 當即執行的狀況下,第一次:cacheTimer => false
      // 當即執行的狀況下,第二次:cacheTimer => true,由於直到delay毫秒後,timer纔會被修改,cacheTimer 變爲false
      timer = setTimeout(() => {
        timer = null
        // delay後,timer重新改成null,則知足條件!cacheTimer,則fn會再次執行
      }, delay)
      if(!cacheTimer) {
        // 緩存了timer,因此當即執行的狀況,第一次緩存的timer時false,會當即執行fn
        fn.call(this, ...args)
      }
    }
  }
}
複製代碼

案例1

<!DOCTYPE html>
<html lang="en">
<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>Document</title>
</head>
<body>
<div class="div">點擊</div>
<script>
  const dom = document.getElementsByClassName('div')[0]
  const fn = () => {
    console.log(11111111111)
  }
 
  dom.addEventListener('click', debounce(fn, 1000, true), false)
  // document.addEventListener('click', (() => debounce(fn, 1000))(), false)
  // 注意:這裏debounce(fn, 1000)會當即執行,返回閉包函數
  // 注意:閉包函數纔是在每次點擊的時候觸發
  
  function debounce(fn, delay, immediate) {
    let timer = null
    return (...args) => { // 這裏能夠拿到事件對象
      if (immediate && !timer) {
        // 若是當即執行標誌位是 true,而且timer不存在
        // 即第一次觸發的狀況
        // 之後的觸發因爲timer存在,則再也不進入執行
        console.log('第一次當即執行')
        fn.call(this, ...args)
      }
      if (timer) {
        clearTimeout(timer)
        // timer存在,就清除定時器
        // 清除定時器,則定時器對應的回調函數也就不會執行
      }
      timer = setTimeout(() => {
        console.log(args, 'args')
        console.log(this, 'this')
        fn.call(this, ...args)
      }, delay)
    }
  }
</script>
</body>
</html>
複製代碼

案例二 - react中

  • 手動取消
function App() {
  const fn = () => {
    console.log('fn')
  }
  const debounce = (fn, delay, immediate) => {
    let timer = null
    return (...args) => {
      if (immediate && !timer) {
        fn.call(this, ...args)
      }
      if (timer) {
        clearTimeout(timer)
      }
      timer = setTimeout(() => {
        fn.call(this, ...args)
      }, delay)
      debounce.cancel = function () { // 手動取消debounce
        clearTimeout(timer)
      }
    }
  }
  const cancleDebounce = () => {
    debounce.cancel()
  }
  return (
    <div className="App">
      <div onClick={debounce(fn, 3000, true)}>點擊2</div>
      <div onClick={cancleDebounce}>取消執行</div>
    </div>
  );
}
複製代碼

在真實項目中的運用

  • 如視頻監聽斷流的回調,會不停的執行監聽函數,當視頻當斷流時,就再也不執行監聽函數了,此時能夠用debounce,就能處理監聽到斷流後須要處理的事情,好比提示斷流
  • input框的查詢結果,不須要輸入每一個字符都去查詢結果,而是使用debounce函數去處理查詢後端接口
  • 小結:Debounce須要考慮第一次執行,手動取消執行,事件對象event等參數的傳遞問題

Throttle

  • 特色:每隔一段時間,只執行一次
  • 在時間a內,只會執行一次函數,屢次觸發也只會觸發一次

版本一(基礎版本)

  • 原理:設置一個標誌位爲true,在閉包中判斷標誌位,false則turn;接着把表示爲改成false,第二次就直接返回了,不會執行定時器,定時器執行完,標誌位改成true,則又能夠進入閉包執行定時器
function throttle(fn, delay) {
    let isRun = true // 標誌位
    return (...args) => {
      if (!isRun) { // false則跳出函數,再也不向下執行
        return
      }
      isRun = false // 當即改成false,則下次不會再執行到定位器,直到定時器執行完,isRun爲true,纔有機會執行到定時器
      let timer = setTimeout(() => {
        fn.call(this, ...args)
        isRun = true
        clearTimeout(timer) // 執行完全部操做後,清除定時器
      }, delay)
    }
  }
複製代碼

版本二(利用時間戳)

  • 原理:比較兩次點擊的時間戳差值(單位是毫秒),大於delay毫秒則執行fn
function throttle(fn, delay) {
  let previous = 0 // 緩存上一次的時間戳
  return (...args) => {
    const now = + new Date()
    // (+)一元加運算符:能夠把任意類型的數據轉換成(數值),結果只能是(數值)和(NaN)兩種
    // 獲取如今的時間戳,即距離1970.1.1 00:00:00的毫秒數字
    // 注意:單位是毫秒數,和定時器的第二個參數吻合,也是毫秒數
    if (now - previous > delay) {
     // 第一次:now - previous > delay是true,因此當即執行一次
     // 而後 previous = now
     // 第二次:第二次能進來的條件就是差值毫秒數超過delay毫秒
     // 這樣頻繁的點擊時,就能按照固定的頻率執行,固然是下降了頻率
      fn.call(this, ...args)
      previous = now // 注意:執行完記得同步時間
    }
  }
}
複製代碼

在真實項目中的運用

  • 瀏覽器窗口的resize
  • 滾動條的滾動監聽函數須要觸發的回調
  • 上拉加載更多

underscore中的Throttle

前置知識:
- leading:是頭部,領導的意思
- trailing: 是尾部的意思
- remaining:剩餘的意思 (remain:剩餘)


options.leading  => 布爾值,表示是否執行事件剛開始的那次回調,false表示不執行開始時的回調
options.trailing => 布爾值,表示是否執行事件結束時的那次回調,false表示不執行結束時的回調



_.throttle = function(func, wait, options) {
  // func:throttle函數觸發時須要執行的函數
  // wait:定時器的延遲時間
  // options:配置對象,有 leading 和 trailing 屬性

  var timeout, context, args, result;
  // timeout:定時器ID
  // context:上下文環境,用來固定this
  // args:傳入func的參數
  // result:func函數執行的返回值,由於func是可能存在返回值的,因此須要考慮到返回值的賦值

  
  var previous = 0;
  // 記錄上一次事件觸發的時間戳,用來緩存每一次的 now
  // 第一次是:0
  // 之後就是:上一次的時間戳
  
  if (!options) options = {};
  // 配置對象不存在,就設置爲空對象


  var later = function() { // later是定時器的回調函數
    previous = options.leading === false ? 0 : _.now();
    timeout = null; // 從新賦值爲null,用於條件判斷,和下面的操做同樣
    result = func.apply(context, args);
    if (!timeout) context = args = null;
    // timer必然爲null,上面從新賦值了,重置context, args
  };

  var throttled = function() {
  
    var now = _.now();
    // 獲取當前時間的時間戳
    
    if (!previous && options.leading === false) previous = now;
    // 若是previous不存在,而且第一次回調不須要執行的話,previous = now
    // previous
        // 第一次是:previous = 0
        // 以後都是:previous是上次的時間戳
    // options.leading === false
        // 注意:這裏是三等,即類型不同的話都是false
        // 因此:leading是undefined時,undefined === false 結果是 fale,由於類型都不同
    
    var remaining = wait - (now - previous);
    // remaining:表示距離下次觸發 func 還需等待的時間
    // remaining的值的取值狀況,下面有分析
    
    context = this;
    // 固定this指向
    
    args = arguments;
    // 獲取func的實參
    
    if (remaining <= 0 || remaining > wait) {
      // remaining <= 0 的全部狀況以下:
        // 狀況1:
            // 第一次觸發,而且(不傳options或傳入的options.leading === true)即須要當即執行第一次回調
            // remaining = wait - (now - 0) => remaining = wait - now 必然小於0
        // 狀況2: 
            // now - previous > wait,即間隔的時間已經大於了傳入定時器的時間
      // remaining > wait 的狀況以下:
        // 說明 now < previous 正常狀況時絕對不會出現的,除非修改了電腦的本地時間,能夠直接不考慮
      
      if (timeout) {
        // 定時器ID存在,就清除定時器
        clearTimeout(timeout);
        timeout = null;
        // 清除定時器後,將timeout設置爲null,這樣就不會再次進入這個if語句
        // 注意:好比 var xx = clearTimeout(aa),這裏clearTimeout()不會把xx變成null,xx不會改變,可是aa不會執行
        
      }
      previous = now;
      // 立刻緩存now,在執行func以前
      
      result = func.apply(context, args);
      // 執行func
      
      if (!timeout) context = args = null;
      // 定時器ID不存在,就重置context和args
      // 注意:這裏timeout不是必定爲null的
        // 1. 若是進入了上面的if語句,就會被重置爲null
        // 2. 果如沒有進入上面的if語句,則有多是有值的
      
    } else if (!timeout && options.trailing !== false) {
      // 定時器ID不存在 而且 最後一次回調須要觸發時進入
      // later是回調
      timeout = setTimeout(later, remaining);
    }
    return result;
    // 返回func的返回值
  };

  throttled.cancel = function() { // 取消函數
    clearTimeout(timeout); // 清除定時器
    
    // 如下都是重置一切參數
    previous = 0;
    timeout = context = args = null;
  };

  return throttled;
};



----------------------------------------------------------
總結整個流程:
window.onscroll = _.throttle(fn, 1000);
window.onscroll = _.throttle(fn, 1000, {leading: false});
window.onscroll = _.throttle(fn, 1000, {trailing: false});

以點擊觸發_.throttle(fn, 1000)爲例:
1. 第一次點擊
(1)now賦值
(2)不會執行previous = now
(3)remaining = wait - now => remain < 0
(4)進入if (remaining <= 0 || remaining > wait) 中
(5)previous = now;
(6)執行 func.apply(context, args)
(7)context = args = null
2. 第二次點擊 - 迅速的
(1)now賦值
(2)進入if (!timeout && options.trailing !== false) 中

(3)timeout = setTimeout(later, remaining); 
    // 特別注意:這時timerout有值了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    // 而 timeout = null的賦值一共有兩處!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    // (1)if (remaining <= 0 || remaining > wait) 這個if中修改!!!!!!!!!!!!!!!!!
    // (2)if (!timeout && options.trailing !== false)這個if的定時器回調中修改!!!!!!!!!!
    //  而(2)中的定時器回調須要在remaining毫秒後纔會修改!!!!!!!!!!!!!!!!!!!!!!!
    
(4)previous = _.now(); 而後 timeout = null; 在而後 result = func.apply(context, args);
(5)context = args = null;
3. 第三次點擊 - 迅速的
- 由於在timeout存在,remaining毫秒還未到時,不會進入任何條件語句中執行任何代碼
- 直到定時器時間到後,修改了timeout = null,previous被從新修改後就再作判斷
複製代碼

Debounce: juejin.im/post/5c270a…
Throttle: juejin.im/post/5be24d…
分析underscore-throttle1:juejin.im/post/5cedd3…
分析underscore-throttle2:github.com/lessfish/un…
underscore源碼地址:github.com/jashkenas/u…
juejin.im/post/5d0a53…react

相關文章
相關標籤/搜索