[深入10] Debounce Throttle

導(dǎo)航

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

[react] Hooks

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

[源碼-webpack01-前置知識] AST抽象語法樹
[源碼-webpack02-前置知識] Tapable
[源碼-webpack03] 手寫webpack - compiler簡單編譯流程
[源碼] Redux React-Redux01
[源碼] axios
[源碼] vuex
[源碼-vue01] data響應(yīng)式 和 初始化渲染

Debounce 防抖函數(shù)

  • <font color=red>特點:延時執(zhí)行,如果在延時的時間內(nèi)多次觸發(fā),則從新計時</font>
  • 過程:當(dāng)事件A發(fā)生時,設(shè)置一個定時器,a秒后觸發(fā)A的回調(diào)函數(shù),如果在a秒內(nèi)有新的同一事件發(fā)生,則清除定時器,并從新開始計時(即又在a秒后觸發(fā)A的回調(diào),注意:上次的A的回調(diào)并未觸發(fā),而是定時器被清除了,定時器中A的回調(diào)就不會被執(zhí)行)

版本一 (基礎(chǔ)版本)

  • <font color=red>優(yōu)點:可以傳參,比如點擊時,點擊事件提供的 event 對象</font>
  • <font color=red>缺點:</font>
    • <font color=red>第一次觸發(fā)是不需要延時的,版本一的第一次也是需要定時器的delay時間后才會執(zhí)行</font>
    • <font color=red>不能手動取消debounce的執(zhí)行,在delay時間未到時的最后一次的執(zhí)行</font>
版本一 (基礎(chǔ)版本)

/**
 * @param {function} fn 需要debounce防抖函數(shù)處理的函數(shù)
 * @param {number} delay 定時器延時的時間
 */
function debounce(fn, delay) {
    let timer = null 
    // 該變量常駐內(nèi)存,可以記住上一次的狀態(tài)
    // 只有在外層函數(shù)失去引用時,該變量才會清除
    // 緩存定時器id
    
    return (...args) => {
      // 返回一個閉包
      // 注意參數(shù):比如事件對象 event 能夠獲取到
      if (timer) {
        // timer存在,就清除定時器
        // 清除定時器,則定時器對應(yīng)的回調(diào)函數(shù)也就不會執(zhí)行
        clearTimeout(timer)
      }
      // 清除定時器后,重新計時
      timer = setTimeout(() => {
        fn.call(this, ...args)
        // this需要定時器回調(diào)函數(shù)時才能確定,this指向調(diào)用時所在的對象,大多數(shù)情況都指向window
      }, delay)
    }
  }

版本二 (升級版本)

  • <font color=red>解決問題:解決第一次點擊不能立即觸發(fā)的問題</font>
  • <font color=red>解決問題:在delay時間沒有到時,手動的取消debounce的執(zhí)行</font>
  • 實現(xiàn)的結(jié)果:
    • 第一次點擊立即觸發(fā)
    • 如果從第一次點擊開始,一直不間斷頻繁點擊(未超過delay時間),然后停止點擊不再點擊,會觸發(fā)兩次,第一次是立即執(zhí)行的,第二次是debounce延時執(zhí)行的
    • 可以手動取消debounce的執(zhí)行 其實就是手動清除最后一次的timer
版本二 (升級版本)

/**
 * @param {function} fn 需要debounce防抖函數(shù)處理的函數(shù)
 * @param {number} delay 定時器延時的時間
 * @param {boolean} immediate 是否立即執(zhí)行
 */
function debounce(fn, delay, immediate) {
    let timer = null
    return (...args) => { // 這里可以拿到事件對象
      if (immediate && !timer) {
        // 如果立即執(zhí)行標(biāo)志位是 true,并且timer不存在
        // 即第一次觸發(fā)的情況
        // 以后的觸發(fā)由于timer存在,則不再進入執(zhí)行
        // 注意:timer是setTimeout()執(zhí)行返回的值,不是setTimeout()的回調(diào)執(zhí)行時才返回,是立即返回的
        // 注意:所以第二次觸發(fā)時,timer就已經(jīng)有值了,不是setTimeout()的回調(diào)執(zhí)行時才返回
        fn.call(this, ...args)
        // 解決:
        // timer = 1
        // return
      }
      if (timer) {
        clearTimeout(timer)
        // timer存在,就清除定時器
        // 清除定時器,則定時器對應(yīng)的回調(diào)函數(shù)也就不會執(zhí)行
      }
      timer = setTimeout(() => {
        console.log(args, 'args')
        console.log(this, 'this')
        fn.call(this, ...args)
        // 注意:有一個特殊情況
        // 比如:只點擊一次,在上面的immediate&&!timer判斷中會立即執(zhí)行一次,然后在delay后,定時器中也會觸發(fā)一次
        // 如何解決執(zhí)行兩次: 在上面的immediate&&!timer判斷中立即執(zhí)行一次fn后,將timer=1,同時return,將不再往下執(zhí)行,同時timer存在
        
        // --------------------
        // if (!immediate) {
        //  fn.call(this, ...args)
        // }
        // immediate = false
        // 注釋的操作可以只在點擊一次沒有再點擊的情況只執(zhí)行一次
        // 但是:一次性多次點擊,第二次不會觸發(fā),只有再停頓達到delay后,再次點擊才會正常的達到debounce的效果
         // --------------------
        
      }, delay)
      
      // 手動取消執(zhí)行debounce函數(shù)
      debounce.cancel = function () {
        clearTimeout(timer)
      }
    }
  }

<font color=red>版本三 (變更需求)</font>

  • <font color=red>需求:第一次立即執(zhí)行,然后等到停止觸發(fā)delay毫秒后,才可以重新觸發(fā)</font>
版本三 (變更需求)
需求:第一次立即執(zhí)行,然后等到停止觸發(fā)delay毫秒后,才可以重新觸發(fā)

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

    if (timer) {
      clearTimeout(timer)
    }

    if(!immediate) {
      // 不立即執(zhí)行的情況
      // 和最初的版本一樣
      timer = setTimeout(() => {
        fn.call(this, ...args)
      }, delay)
    } else {
      // 立即執(zhí)行
      const cacheTimer = timer // 緩存timer
      // 緩存timer, 因為下面timer會立即改變,如果直接用timer判斷,fn不會執(zhí)行
      // 立即執(zhí)行的情況下,第一次:cacheTimer => false
      // 立即執(zhí)行的情況下,第二次:cacheTimer => true,因為直到delay毫秒后,timer才會被修改,cacheTimer 變?yōu)閒alse
      timer = setTimeout(() => {
        timer = null
        // delay后,timer從新改為null,則滿足條件!cacheTimer,則fn會再次執(zhí)行
      }, delay)
      if(!cacheTimer) {
        // 緩存了timer,所以立即執(zhí)行的情況,第一次緩存的timer時false,會立即執(zhí)行fn
        fn.call(this, ...args)
      }
    }
  }
}
image

案例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)會立即執(zhí)行,返回閉包函數(shù)
  // 注意:閉包函數(shù)才是在每次點擊的時候觸發(fā)
  
  function debounce(fn, delay, immediate) {
    let timer = null
    return (...args) => { // 這里可以拿到事件對象
      if (immediate && !timer) {
        // 如果立即執(zhí)行標(biāo)志位是 true,并且timer不存在
        // 即第一次觸發(fā)的情況
        // 以后的觸發(fā)由于timer存在,則不再進入執(zhí)行
        console.log('第一次立即執(zhí)行')
        fn.call(this, ...args)
      }
      if (timer) {
        clearTimeout(timer)
        // timer存在,就清除定時器
        // 清除定時器,則定時器對應(yīng)的回調(diào)函數(shù)也就不會執(zhí)行
      }
      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}>取消執(zhí)行</div>
    </div>
  );
}

在真實項目中的運用

  • <font color=red>如視頻監(jiān)聽斷流的回調(diào),會不停的執(zhí)行監(jiān)聽函數(shù),當(dāng)視頻當(dāng)斷流時,就不再執(zhí)行監(jiān)聽函數(shù)了,此時可以用debounce,就能處理監(jiān)聽到斷流后需要處理的事情,比如提示斷流</font>
  • input框的查詢結(jié)果,不需要輸入每個字符都去查詢結(jié)果,而是使用debounce函數(shù)去處理查詢后端接口
  • <table><tr><td bgcolor=orange>小結(jié):Debounce需要考慮第一次執(zhí)行,手動取消執(zhí)行,事件對象event等參數(shù)的傳遞問題</table></tr></td>

Throttle

  • <font color=red>特點:每隔一段時間,只執(zhí)行一次</font>
  • 在時間a內(nèi),只會執(zhí)行一次函數(shù),多次觸發(fā)也只會觸發(fā)一次

版本一(基礎(chǔ)版本)

  • 原理:設(shè)置一個標(biāo)志位為true,在閉包中判斷標(biāo)志位,false則turn;接著把表示為改為false,第二次就直接返回了,不會執(zhí)行定時器,定時器執(zhí)行完,標(biāo)志位改為true,則又可以進入閉包執(zhí)行定時器;同時定時器執(zhí)行完,要清除定時器

  function throttle(fn, delay) {
    let isRun = true // 標(biāo)志位
    return (...args) => {
      if (!isRun) { // false則跳出函數(shù),不再向下執(zhí)行
        return
      }
      isRun = false // 立即改為false,則下次不會再執(zhí)行到定位器,直到定時器執(zhí)行完,isRun為true,才有機會執(zhí)行到定時器
      let timer = setTimeout(() => {
        fn.call(this, ...args)
        isRun = true
        clearTimeout(timer) // 執(zhí)行完所有操作后,清除定時器
      }, delay)
    }
  }

版本二(利用時間戳)

  • <font color=red>原理:比較兩次點擊的時間戳差值(單位是毫秒),大于delay毫秒則執(zhí)行fn</font>

function throttle(fn, delay) {
  let previous = 0 // 緩存上一次的時間戳
  return (...args) => {
    const now = + new Date()
    // (+)一元加運算符:可以把任意類型的數(shù)據(jù)轉(zhuǎn)換成(數(shù)值),結(jié)果只能是(數(shù)值)和(NaN)兩種
    // 獲取現(xiàn)在的時間戳,即距離1970.1.1 00:00:00的毫秒數(shù)字
    // 注意:單位是毫秒數(shù),和定時器的第二個參數(shù)吻合,也是毫秒數(shù)
    if (now - previous > delay) {
     // 第一次:now - previous > delay是true,所以立即執(zhí)行一次
     // 然后 previous = now
     // 第二次:第二次能進來的條件就是差值毫秒數(shù)超過delay毫秒
     // 這樣頻繁的點擊時,就能按照固定的頻率執(zhí)行,當(dāng)然是降低了頻率
      fn.call(this, ...args)
      previous = now // 注意:執(zhí)行完記得同步時間
    }
  }
}

在真實項目中的運用

  • 瀏覽器窗口的resize
  • 滾動條的滾動監(jiān)聽函數(shù)需要觸發(fā)的回調(diào)
  • 上拉加載更多

<font color=red>underscore中的Throttle</font>

前置知識:
- leading:是頭部,領(lǐng)導(dǎo)的意思
- trailing: 是尾部的意思
- remaining:剩余的意思 (remain:剩余)


options.leading  => 布爾值,表示是否執(zhí)行事件剛開始的那次回調(diào),false表示不執(zhí)行開始時的回調(diào)
options.trailing => 布爾值,表示是否執(zhí)行事件結(jié)束時的那次回調(diào),false表示不執(zhí)行結(jié)束時的回調(diào)



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

  var timeout, context, args, result;
  // timeout:定時器ID
  // context:上下文環(huán)境,用來固定this
  // args:傳入func的參數(shù)
  // result:func函數(shù)執(zhí)行的返回值,因為func是可能存在返回值的,所以需要考慮到返回值的賦值

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


  var later = function() { // later是定時器的回調(diào)函數(shù)
    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();
    // 獲取當(dāng)前時間的時間戳
    
    if (!previous && options.leading === false) previous = now;
    // 如果previous不存在,并且第一次回調(diào)不需要執(zhí)行的話,previous = now
    // previous
        // 第一次是:previous = 0
        // 之后都是:previous是上次的時間戳
    // options.leading === false
        // 注意:這里是三等,即類型不一樣的話都是false
        // 所以:leading是undefined時,undefined === false 結(jié)果是 fale,因為類型都不一樣
    
    var remaining = wait - (now - previous);
    // remaining:表示距離下次觸發(fā) func 還需等待的時間
    // remaining的值的取值情況,下面有分析
    
    context = this;
    // 固定this指向
    
    args = arguments;
    // 獲取func的實參
    
    if (remaining <= 0 || remaining > wait) {
      // remaining <= 0 的所有情況如下:
        // 情況1:
            // 第一次觸發(fā),并且(不傳options或傳入的options.leading === true)即需要立即執(zhí)行第一次回調(diào)
            // remaining = wait - (now - 0) => remaining = wait - now 必然小于0
        // 情況2: 
            // now - previous > wait,即間隔的時間已經(jīng)大于了傳入定時器的時間
      // remaining > wait 的情況如下:
        // 說明 now < previous 正常情況時絕對不會出現(xiàn)的,除非修改了電腦的本地時間,可以直接不考慮
      
      if (timeout) {
        // 定時器ID存在,就清除定時器
        clearTimeout(timeout);
        timeout = null;
        // 清除定時器后,將timeout設(shè)置為null,這樣就不會再次進入這個if語句
        // 注意:比如 var xx = clearTimeout(aa),這里clearTimeout()不會把xx變成null,xx不會改變,但是aa不會執(zhí)行
        
      }
      previous = now;
      // 馬上緩存now,在執(zhí)行func之前
      
      result = func.apply(context, args);
      // 執(zhí)行func
      
      if (!timeout) context = args = null;
      // 定時器ID不存在,就重置context和args
      // 注意:這里timeout不是一定為null的
        // 1. 如果進入了上面的if語句,就會被重置為null
        // 2. 果如沒有進入上面的if語句,則有可能是有值的
      
    } else if (!timeout && options.trailing !== false) {
      // 定時器ID不存在 并且 最后一次回調(diào)需要觸發(fā)時進入
      // later是回調(diào)
      timeout = setTimeout(later, remaining);
    }
    return result;
    // 返回func的返回值
  };

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

  return throttled;
};



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

以點擊觸發(fā)_.throttle(fn, 1000)為例:
1. 第一次點擊
(1)now賦值
(2)不會執(zhí)行previous = now
(3)remaining = wait - now => remain < 0
(4)進入if (remaining <= 0 || remaining > wait) 中
(5)previous = now;
(6)執(zhí)行 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的定時器回調(diào)中修改!?。。。。。。。。?    //  而(2)中的定時器回調(diào)需要在remaining毫秒后才會修改?。。。。。。。。。。。。。。。。。。。。。?!
    
(4)previous = _.now(); 然后 timeout = null; 在然后 result = func.apply(context, args);
(5)context = args = null;
3. 第三次點擊 - 迅速的
- 因為在timeout存在,remaining毫秒還未到時,不會進入任何條件語句中執(zhí)行任何代碼
- 直到定時器時間到后,修改了timeout = null,previous被重新修改后就再做判斷

Debounce: https://juejin.im/post/6844903749429919758#heading-0
Throttle: https://juejin.im/post/6844903705763020807
分析underscore-throttle1:https://juejin.im/post/6844903855667281928#heading-3
分析underscore-throttle2:https://github.com/lessfish/underscore-analysis/issues/22
underscore源碼地址:https://github.com/jashkenas/underscore/blob/master/underscore.js
https://juejin.im/post/6844903871504973837

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 導(dǎo)航 [深入01] 執(zhí)行上下文[https://juejin.im/post/684490404605093479...
    woow_wu7閱讀 283評論 0 1
  • 一年前寫了一篇JavaScript八張思維導(dǎo)圖,主要是對前端JavaScript知識點的一個系統(tǒng)的整理和總結(jié)。本篇...
    dykingdy閱讀 1,959評論 0 0
  • 1.關(guān)于閉包 什么是閉包?閉包是有權(quán)限訪問其它函數(shù)作用域內(nèi)的變量的一個函數(shù)。 在js中,變量分為全局變量和局部變量...
    Reusdong閱讀 250評論 0 1
  • 瀏覽器的緩存 強緩存exprices 絕對時間 格林尼治時間 時間點cache-control 相對時間 優(yōu)先級高...
    daybreakcold閱讀 327評論 0 0
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭,有人歡樂有人憂愁,有人驚喜有人失落,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,809評論 28 54

友情鏈接更多精彩內(nèi)容