1. 程式人生 > >js函式節流和去抖

js函式節流和去抖

前言

函式節流和去抖的出現場景,一般都伴隨著客戶端 DOM 的事件監聽。舉個例子,實現一個原生的拖拽功能(不能用 H5 Drag&Drop API),需要一路監聽 mousemove 事件,在回撥中獲取元素當前位置,然後重置 dom 的位置(樣式改變)。如果我們不加以控制,每移動一定畫素而觸發的回撥數量是會非常驚人的,回撥中又伴隨著 DOM 操作,繼而引發瀏覽器的重排與重繪,效能差的瀏覽器可能就會直接假死,這樣的使用者體驗是非常糟糕的。我們需要做的是降低觸發回撥的頻率,比如讓它 500ms 觸發一次,或者 200ms,甚至 100ms,這個閾值不能太大,太大了拖拽就會失真,也不能太小,太小了低版本瀏覽器可能就會假死,這樣的解決方案就是函式節流,英文名字叫「throttle」。函式節流的核心是,讓一個函式不要執行得太頻繁,減少一些過快的呼叫來節流。

說完函式節流,再看它的好基友函式去抖(debounce)。思考這樣一個場景,對於瀏覽器視窗,每做一次 resize 操作,傳送一個請求,很顯然,我們需要監聽 resize 事件,但是和 mousemove 一樣,每縮小(或者放大)一次瀏覽器,實際上會觸發 N 多次的 resize 事件,用節流?節流只能保證定時觸發,我們一次就好,這就要用去抖。簡單的說,函式去抖就是對於一定時間段的連續的函式呼叫,只讓其執行一次。

throttle像是按鈕的冷卻時間,防止使用者頻繁操作;
debounce像是搜尋框的查詢,等待使用者完成操作再執行,避免打字期間就不斷的查詢。

函式節流應用場景

函式節流有哪些應用場景?哪些時候我們需要間隔一定時間觸發回撥來控制函式呼叫頻率?

  • DOM 元素的拖拽功能實現(mousemove)
  • 射擊遊戲的 mousedown/keydown 事件(單位時間只能發射一顆子彈)
  • 計算滑鼠移動的距離(mousemove)
  • Canvas 模擬畫板功能(mousemove)
  • 搜尋聯想(keyup)
  • 監聽滾動事件判斷是否到頁面底部自動載入更多:給 scroll 加了 debounce 後,只有使用者停止滾動後,才會判斷是否到了頁面底部;如果是 throttle 的話,只要頁面滾動就會間隔一段時間判斷一次

函式去抖應用場景

函式去抖有哪些應用場景?哪些時候對於連續的事件響應我們只需要執行一次回撥?

  • 每次 resize/scroll 觸發統計事件
  • 文字輸入的驗證(連續輸入文字後傳送 AJAX 請求進行驗證,驗證一次就好)

節流例項程式碼

由於節流是改變回調函式被頻繁觸發,讓它可以隔一段時間觸發一次,而不是在操作的過程中一直被觸發。下面是最簡單的節流實現:

 _throttle(method, delay = 100) {

        const that = this;
        let last = 0;

        return function () {

            const now = Date.now();

            if (now - last > delay) {
                method.apply(that, arguments);
                last = now;
            }

        }
    }

這裡的delay是每次觸發回撥函式的間隔時間。例如,每次進行scroll滾動操作時,先獲取當前時間,減去0,計算的就是上一次呼叫這個回撥函式和現在的間隔時間,時間大於delay這個時間,才執行回撥函式,然後把last的這個時間改為如今執行函式的時間。

去抖例項程式碼

去抖就是在進行操作時避免一直觸發回撥函式,只在操作結束的時候才執行回撥函式。

 window.onscroll = function(){
        console.log("scroll滑動");
        throttle(count);
    }

    function count(){
        console.log("函式呼叫");
    }

    function throttle(method, context){
        clearTimeout(method.tId)
        method.tId = setTimeout(function(){
            method.call(context);
        }, 300);
    }

每次執行scroll操作時,會一直觸發throttle(count)方法,每次執行throttle,會先把之前的定時器清除,再開啟一個新的定時器,由於觸發throttle(count)的頻率是很高的,因此定時器還沒來得及執行,已經被下一次的throttle(count)事件清除了,直到scroll停止了,最後就會觸發一次回撥函式。

用節流優化去抖

去抖的問題在於它“太有耐心了”。試想,如果使用者的操作十分頻繁——他每次都不等 去抖 設定的 delay 時間結束就進行下一次操作,於是每次 去抖 都為該使用者重新生成定時器,回撥函式被延遲了不計其數次。頻繁的延遲會導致使用者遲遲得不到響應,使用者同樣會產生“這個頁面卡死了”的觀感。 為了避免弄巧成拙,我們需要借力 節流 的思想,打造一個“有底線”的 去抖——等你可以,但我有我的原則:delay 時間內,我可以為你重新生成定時器;但只要delay的時間到了,我必須要給使用者一個響應。這個 節流 與 去抖 “合體”思路,已經被很多成熟的前端庫應用到了它們的加強版 節流 函式的實現中:

// fn是我們需要包裝的事件回撥, delay是每次推遲執行的等待時間
function debounce(fn, delay) {
  // 定時器
  let timer = null
  
  // 將debounce處理結果當作函式返回
  return function () {
    // 保留呼叫時的this上下文
    let context = this
    // 保留呼叫時傳入的引數
    let args = arguments

    // 每次事件被觸發時,都去清除之前的舊定時器
    if(timer) {
        clearTimeout(timer)
    }
    // 設立新定時器
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}

// 用debounce來包裝scroll的回撥
document.addEventListener('scroll', debounce(() => console.log('觸發了滾動事件'), 1000))