JavaScript防抖節(jié)流原理

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

一、防抖debounce

你是否在日常開(kāi)發(fā)中遇到一個(gè)問(wèn)題,在滾動(dòng)事件中需要做個(gè)復(fù)雜計(jì)算或者實(shí)現(xiàn)一個(gè)按鈕的防二次點(diǎn)擊操作

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

持續(xù)觸發(fā)scroll事件時(shí),并不執(zhí)行handle函數(shù),當(dāng)1000毫秒內(nèi)沒(méi)有觸發(fā)scroll事件時(shí),才會(huì)延時(shí)觸發(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()); 
}
// 滾動(dòng)事件
// 當(dāng)持續(xù)觸發(fā)scroll事件時(shí),事件處理函數(shù)handle只在停止?jié)L動(dòng)1000毫秒之后才會(huì)調(diào)用一次,也就是說(shuō)在持續(xù)觸發(fā)scroll事件的過(guò)程中,事件處理函數(shù)handle一直沒(méi)有執(zhí)行
window.addEventListener('scroll', debounce(handle, 1000));

我們先來(lái)看一個(gè)袖珍版的防抖理解一下防抖的實(shí)現(xiàn)

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

這是一個(gè)簡(jiǎn)單版的防抖,但是有缺陷,這個(gè)防抖只能在最后調(diào)用。一般的防抖會(huì)有immediate選項(xiàng),表示是否立即調(diào)用。這兩者的區(qū)別,舉個(gè)栗子來(lái)說(shuō)

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

完整代碼

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

// 這個(gè)是用來(lái)獲取當(dāng)前時(shí)間戳的
function now() {
  return +new Date()
}
/**
 * 防抖函數(shù),返回函數(shù)連續(xù)調(diào)用時(shí),空閑時(shí)間必須大于或等于 wait,func 才會(huì)執(zhí)行
 *
 * @param  {function} func        回調(diào)函數(shù)
 * @param  {number}   wait        表示時(shí)間窗口的間隔
 * @param  {boolean}  immediate   設(shè)置為ture時(shí),是否立即調(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í)行完畢,清空緩存的定時(shí)器序號(hào)
    timer = null
    // 延遲執(zhí)行的情況下,函數(shù)會(huì)在延遲函數(shù)中執(zhí)行
    // 使用到之前緩存的參數(shù)和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 這里返回的函數(shù)是每次實(shí)際調(diào)用的函數(shù)
  return function(...params) {
    // 如果沒(méi)有創(chuàng)建延遲執(zhí)行函數(shù)(later),就創(chuàng)建一個(gè)
    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í)候清除原來(lái)的并重新設(shè)定一個(gè)
    // 這樣做延遲函數(shù)會(huì)重新計(jì)時(shí)
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}
  • 對(duì)于按鈕防點(diǎn)擊來(lái)說(shuō)的實(shí)現(xiàn):如果函數(shù)是立即執(zhí)行的,就立即調(diào)用,如果函數(shù)是延遲執(zhí)行的,就緩存上下文和參數(shù),放到延遲函數(shù)中去執(zhí)行。一旦我開(kāi)始一個(gè)定時(shí)器,只要我定時(shí)器還在,你每次點(diǎn)擊我都重新計(jì)時(shí)。一旦你點(diǎn)累了,定時(shí)器時(shí)間到,定時(shí)器重置為 null,就可以再次點(diǎn)擊了。
  • 對(duì)于延時(shí)執(zhí)行函數(shù)來(lái)說(shuō)的實(shí)現(xiàn):清除定時(shí)器ID,如果是延遲調(diào)用就調(diào)用函數(shù)

二、節(jié)流throttle

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

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

image.png

節(jié)流版本

/**
 * underscore 節(jié)流函數(shù),返回函數(shù)連續(xù)調(diào)用時(shí),func 執(zhí)行頻率限定為 次 / wait
 *
 * @param  {function}   func      回調(diào)函數(shù)
 * @param  {number}     wait      表示時(shí)間窗口的間隔
 * @param  {object}     options   如果想忽略開(kāi)始函數(shù)的的調(diào)用,傳入{leading: false}。
 *                                如果想忽略結(jié)尾函數(shù)的調(diào)用,傳入{trailing: false}
 *                                兩者不能共存,否則函數(shù)不能執(zhí)行
 * @return {function}             返回客戶調(diào)用函數(shù)   
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的時(shí)間戳
    var previous = 0;
    // 如果 options 沒(méi)傳則設(shè)為空對(duì)象
    if (!options) options = {};
    // 定時(shí)器回調(diào)函數(shù)
    var later = function() {
      // 如果設(shè)置了 leading,就將 previous 設(shè)為 0
      // 用于下面函數(shù)的第一個(gè) if 判斷
      previous = options.leading === false ? 0 : _.now();
      // 置空一是為了防止內(nèi)存泄漏,二是為了下面的定時(shí)器判斷
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 獲得當(dāng)前時(shí)間戳
      var now = _.now();
      // 首次進(jìn)入前者肯定為 true
      // 如果需要第一次不執(zhí)行函數(shù)
      // 就將上次時(shí)間戳設(shè)為當(dāng)前的
      // 這樣在接下來(lái)計(jì)算 remaining 的值時(shí)會(huì)大于0
      if (!previous && options.leading === false) previous = now;
      // 計(jì)算剩余時(shí)間
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果當(dāng)前調(diào)用已經(jīng)大于上次調(diào)用時(shí)間 + wait
      // 或者用戶手動(dòng)調(diào)了時(shí)間
      // 如果設(shè)置了 trailing,只會(huì)進(jìn)入這個(gè)條件
      // 如果沒(méi)有設(shè)置 leading,那么第一次會(huì)進(jìn)入這個(gè)條件
      // 還有一點(diǎn),你可能會(huì)覺(jué)得開(kāi)啟了定時(shí)器那么應(yīng)該不會(huì)進(jìn)入這個(gè) if 條件了
      // 其實(shí)還是會(huì)進(jìn)入的,因?yàn)槎〞r(shí)器的延時(shí)
      // 并不是準(zhǔn)確的時(shí)間,很可能你設(shè)置了2秒
      // 但是他需要2.2秒才觸發(fā),這時(shí)候就會(huì)進(jìn)入這個(gè)條件
      if (remaining <= 0 || remaining > wait) {
        // 如果存在定時(shí)器就清理掉否則會(huì)調(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) {
        // 判斷是否設(shè)置了定時(shí)器和 trailing
        // 沒(méi)有的話就開(kāi)啟一個(gè)定時(shí)器
        // 并且不能不能同時(shí)設(shè)置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
};
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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