防抖和節流

在前端開發中,有一部分用戶行爲會頻繁的觸發事件,而對於DOM操做,資源加載等耗費性能的處理,極可能會致使卡頓,甚至瀏覽器的崩潰。防抖和節流就是爲了解決這一類的問題。javascript

window.onscroll  = function () {
    //滾動條位置
    let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  console.log('滾動條位置:' + scrollTop);
}
  • 效果以下
    image

從效果上,咱們能夠看到,在頁面滾動的時候,會在短期內觸發屢次綁定事件。html

咱們知道DOM操做是很耗費性能的,若是在監聽中,作了一些DOM操做,那無疑會給瀏覽器形成大量性能損失。前端

下面咱們進入主題,一塊兒來探究,如何對此進行優化。java

防抖:

  • 理解:在車站上車,人員上滿了車才發走重點是人員上滿觸發一次。瀏覽器

  • 場景:(用戶名檢驗是否已註冊)實時搜索,拖拽。
  • 緣由緩存

  • 定義:屢次觸發事件後,事件處理函數只執行一次,而且是在觸發操做結束時執行。閉包

  • 原理:對處理函數進行延時操做,若設定的延時到來以前,再次觸發事件,則清除上一次的延時操做定時器,從新定時。app

防抖的思想以下:

參考連接函數

  • 藉助事件循環隊列和setTimeout來實現只有空閒的時候纔去處理回調函數
  • 使用setTimeout主要是爲了使得處理方法掛在事件循環隊列後面,保證事件循環隊列中的前面的一些操做有時間進行
// 計時器
var timer = false;
// 
window.onscroll = function(){
    clearTimeout(timer);
    timer = setTimeout(function(){
        console.log("防抖");
        console.log(new Date());
    },300);
};

爲何要clearTimeoutoop

每次onscroll的時候,先清除掉計時器.若是不清楚,會致使屢次觸發的時候,實際上是把好屢次的處理方法放在某個時間點後一塊兒執行。

好比下面:

    for (var i = 0; i < 10; i++) {
        (function (i) {
            setTimeout(function () {
                console.log(i);
            }, 3000);
        })(i);
    }

上面代碼在3秒後會一塊兒輸出 1,2,3,4,5,6,7,8,9

而下面的代碼,只會輸出9

var timer2 = false;
    for (var i = 0; i < 10; i++) {
        clearTimeout(timer2);
        (function (i) {
            timer2 = setTimeout(function () {
                console.log(i);
            }, 3000);
        })(i);
    }

這是由於,每次我將上次的timer給清除掉了,也就是我若是後面一樣有處理函數的話,那我就用後面的定時器。

前面定時器沒啥用了,因此直接clearTimeout保證了這種實現。

在解決onscroll問題的時候,若是本身觀察console能夠發現,防抖保證了滾動中止的時候,纔會進行處理,由於滾動中止了,沒有scroll事件了,最後一次timer會被保留,從而進行調用

  • 實現:
//每一次都要清空定時器,從新設置上計時器值,使得計時器每一次都從新開始,直到最後知足條件而且等待delay時間後,纔開始執行handler函數。


 let timer ;
   window.onscroll = function(){
    
       if(timer){
           clearTimeout(timer);
       }
       timer = setTimeout(function(){
           //滾動條位置
           let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
           console.log('滾動條位置'+ scrollTop);
           timer = null;
       
       },2000)
       console.log(timer);//一滾動,timer就有對應的數值,即
       //setTimeout會返回數值,clearTimeout(這個數值),就至關於清除這個定時器
  • 效果以下:滾動結束觸發事件
    image

    簡單版防抖封裝

// func是用戶傳入須要防抖的函數
// wait是等待時間
const debounce = (func, wait = 50) => {
  // 緩存一個定時器id
  let timer = 0
  // 這裏返回的函數是每次用戶實際調用的防抖函數
  // 若是已經設定過定時器了就清空上一次的定時器
  // 開始一個新的定時器,延遲執行用戶傳入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
window.onscroll = debounce(function(){
    let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滾動條位置:' + scrollTop);
    }
,200)
// 不難看出若是用戶調用該函數的間隔小於wait的狀況下,上一次的時間還未到就被清除了,並不會執行函數

這是一個簡單版的防抖,可是有缺陷,這個防抖只能在最後調用。通常的防抖會有immediate選項,表示是否當即調用。這二者的區別,舉個栗子來講:

例如在搜索引擎搜索問題的時候,咱們固然是但願用戶輸入完最後一個字才調用查詢接口,這個時候適用延遲執行的防抖函數,它老是在一連串(間隔小於wait的)函數觸發以後調用。

例如用戶給interviewMap點star的時候,咱們但願用戶點第一下的時候就去調用接口,而且成功以後改變star按鈕的樣子,用戶就能夠立馬獲得反饋是否star成功了,這個狀況適用當即執行的防抖函數,它老是在第一次調用,而且下一次調用必須與前一次調用的時間間隔大於wait纔會觸發。

實現一個帶有當即執行選項的防抖函數

// 這個是用來獲取當前時間戳的
function now() {
  return +new Date()
}
/**
 * 防抖函數,返回函數連續調用時,空閒時間必須大於或等於 wait,func 纔會執行
 *
 * @param  {function} func        回調函數
 * @param  {number}   wait        表示時間窗口的間隔
 * @param  {boolean}  immediate   設置爲ture時,是否當即調用函數
 * @return {function}             返回客戶調用函數
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args

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

  // 這裏返回的函數是每次實際調用的函數
  return function(...params) {
    // 若是沒有建立延遲執行函數(later),就建立一個
    if (!timer) {
      timer = later()
      // 若是是當即執行,調用函數
      // 不然緩存參數和調用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 若是已有延遲執行函數(later),調用的時候清除原來的並從新設定一個
    // 這樣作延遲函數會從新計時
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

總體函數總結一下。

對於==按鈕防點擊來==說的實現:若是函數是當即執行的,就當即調用,若是函數是延遲執行的,就緩存上下文和參數,放到延遲函數中去執行。一旦我開始一個定時器,只要我定時器還在,你每次點擊我都從新計時。一旦你點累了,定時器時間到,定時器重置爲 null,就能夠再次點擊了。
對於延時執行函數來講的實現:清除定時器ID,若是是延遲調用就調用函數

函數防抖的適用性:

經過上面的例子,咱們知道咱們能夠經過函數防抖,解決了屢次觸發事件時的性能問題。

好比,咱們在監聽滾動條位置,控制是否顯示返回頂部按鈕時,就能夠將防抖函數應用其中。

  但依然有些功能並不適用:

當咱們作圖片懶加載(lazyload)時,須要經過滾動位置,實時顯示圖片時,若是使用防抖函數,懶加載(lazyload)函數將會不斷被延時,

只有停下來的時候纔會被執行,對於這種須要實時觸發事件的狀況,就顯得不是很友好了。

下面開始介紹函數節流,經過設定時間片,控制事件函數間斷性的觸發。

節流:

  • 理解:大於等於10分鐘發一次車,重點是必定間隔時間就會觸發一次。

  • 場景:窗口調整(調整大小),頁面滾動(滾動),搶購時瘋狂點擊(鼠標按下)
  • 定義:觸發函數事件後,短期間隔內沒法連續調用,只有上一次函數執行後,過了規定的時間間隔,才能進行下一次的函數調用。

  • 原理:對處理函數進行延時操做,若設定的延時到來以前,再次觸發事件,則清除上一次的延時操做定時器,從新定時。

簡單來講,函數的節流就是經過閉包保存一個標記(canRun = true),在函數的開頭判斷這個標記是否爲true,若是爲true的話就繼續執行函數,不然則 return 掉,判斷完標記後當即把這個標記設爲false,而後把外部傳入的函數的執行包在一個setTimeout中,最後在setTimeout執行完畢後再把標記設置爲true(這裏很關鍵),表示能夠執行下一次的循環了。

當setTimeout還未執行的時候,canRun這個標記始終爲false,在開頭的判斷中被 return 掉。

function throttle(fn, interval = 300) {
let canRun = true;
return function () {
if (!canRun) return;
canRun = false;
setTimeout(() => {
fn.apply(this, arguments);
canRun = true;
}, interval);
};
}

  scroll 的一個簡單例子

let startTime = Date.now(); //開始時間
let time = 500; //間隔時間
let timer;
window.onscroll = function throttle(){
    let currentTime = Date.now();
    if(currentTime - startTime >= time){
        let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
        console.log('滾動條位置:' + scrollTop);
        startTime = currentTime;
    }else{
        clearTimeout(timer);
        timer = setTimeout(function () {
            throttle()
        }, 50);
    }
}

-  效果以下:每隔500毫秒觸發一次事件
 image

  • 封裝實現:
/**
 * 節流函數
 * @param method 事件觸發的操做
 * @param mustRunDelay 間隔多少毫秒須要觸發一次事件
 */
function throttle(method, mustRunDelay) {
    let timer,
        args = arguments,
        start;
    return function loop() {
        let self = this;
        let now = Date.now();
        if(!start){
            start = now;
        }
        if(timer){
            clearTimeout(timer);
        }
        if(now - start >= mustRunDelay){
            method.apply(self, args);
            start = now;
        }else {
            timer = setTimeout(function () {
                loop.apply(self, args);
            }, 50);
        }
    }
}
window.onscroll = throttle(function () {
    let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
    console.log('滾動條位置:' + scrollTop);
},800)

用時間戳+定時器,當第一次觸發事件時立刻執行事件處理函數,最後一次觸發事件後也還會執行一次事件處理函數

上面的代碼一刷新就觸發事件,即打印出'滾動條位置"

這個是關於忽略開始函數的的調用或者忽略結尾函數的節流封裝函數

**
 * underscore 節流函數,返回函數連續調用時,func 執行頻率限定爲 次 / wait
 *
 * @param  {function}   func      回調函數
 * @param  {number}     wait      表示時間窗口的間隔
 * @param  {object}     options   若是想忽略開始函數的的調用,傳入{leading: false}。
 *                                若是想忽略結尾函數的調用,傳入{trailing: false}
 *                                二者不能共存,不然函數不能執行
 * @return {function}             返回客戶調用函數
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 以前的時間戳
    var previous = 0;
    // 若是 options 沒傳則設爲空對象
    if (!options) options = {};
    // 定時器回調函數
    var later = function() {
      // 若是設置了 leading,就將 previous 設爲 0
      // 用於下面函數的第一個 if 判斷
      previous = options.leading === false ? 0 : _.now();
      // 置空一是爲了防止內存泄漏,二是爲了下面的定時器判斷
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 得到當前時間戳
      var now = _.now();
      // 首次進入前者確定爲 true
      // 若是須要第一次不執行函數
      // 就將上次時間戳設爲當前的
      // 這樣在接下來計算 remaining 的值時會大於0
      if (!previous && options.leading === false) previous = now;
      // 計算剩餘時間
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 若是當前調用已經大於上次調用時間 + wait
      // 或者用戶手動調了時間
      // 若是設置了 trailing,只會進入這個條件
      // 若是沒有設置 leading,那麼第一次會進入這個條件
      // 還有一點,你可能會以爲開啓了定時器那麼應該不會進入這個 if 條件了
      // 其實仍是會進入的,由於定時器的延時
      // 並非準確的時間,極可能你設置了2秒
      // 可是他須要2.2秒才觸發,這時候就會進入這個條件
      if (remaining <= 0 || remaining > wait) {
        // 若是存在定時器就清理掉不然會調用二次回調
        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;
    };
  };
相關文章
相關標籤/搜索