1. 程式人生 > 其它 >js函式節流與防抖

js函式節流與防抖

技術標籤:【前端】JS相關知識jsjavascript

一、節流(throttle)與防抖(debounce)的含義

節流:

指連續觸發事件但是在 n 秒中只執行一次函式。即 2n 秒內執行 2 次,3n秒內執行3次… 。節流如字面意思,會稀釋函式的執行頻率。

使用場景:dom元素拖拽,搜尋聯想等等

防抖:

連續的事件響應我們在n秒內只執行一次回撥。如n秒內觸發6次,只在最後一次再執行回撥。

使用場景:文字輸入驗證

二、簡單實現

節流:

function throttle(fn,wait){
 // 首先獲取呼叫throttle時的一個時間戳作為觸發時時間,實現用閉包儲存 pre 變數。
 var
pre = Date.now(); return function(){ var context = this; var args = arguments; var now = Date.now(); if( now - pre >= wait){ // 噹噹前時間-出發時時間大於等待時間後,觸發fn函式執行 fn.apply(context,args); pre = Date.now(); // 更新觸發時間 } else{ //讓方法在脫離事件後也能執行一次 timeout =
setTimeout(function(){ fn.apply(context, args) }, wait); } } }
   由此可以實現,在wait時間範圍內,只執行一次,下一個時間窗內,會再次觸發。呼叫方式:比如在2秒後呼叫handleSth()方法: throttle(handleSth, 2000)

防抖:

// 1、非立即執行版:一開始不觸發delay秒後才會執行
function debounce(func, delay) {
  let timer = null;
  return function() {
   clearTimeout
(timer); timer = setTimeout(() => { func.apply(this, arguments) // 具體分析可見:https://blog.csdn.net/weixin_44494811/article/details/103486637 }, delay) } } // 2、立即執行版:一開始就觸發,後面再觸發不執行,delay秒後可以再觸發 function debounce (func, delay) { let timer; return function(){ clearTimeout(timer); let callNow = !timer timer = setTimeout(() => { timer = null; }, delay) if (callNow) { func.apply(this, arguments); } } } // 3、綜合版 // 合成版 /** * @desc 函式防抖 * @param func 目標函式 * @param wait 延遲執行毫秒數 * @param immediate true - 立即執行, false - 延遲執行 */ function debounce(func, wait, immediate) { let timer; return function() { let context = this, args = arguments; // 根據immediate引數配置是否立即執行 if (timer) clearTimeout(timer); if (immediate) { let callNow = !timer; timer = setTimeout(() => { timer = null; }, wait); if (callNow) func.apply(context, args); } else { timer = setTimeout(() => { func.apply }, wait) } } }

拓展延伸

以上涉及的函式設計和思想可參考更詳細的分解:節流函式的原理和設計關於閉包中變數的儲存

三、Lodash原始碼中的實現

lodash庫中關於節流函式的實現,比簡單的方法要成熟得多,首先,1、對引入的函式方法增加了型別判斷:typeof function, 對配置也增加了物件型別判斷:isObject.js 2、對函式節流函式增加了更多的可配置化引數,比如設定個最長等待時間,不管如何先響應一次再防抖等等,避免使用者看上次頁面假死。

1、節流:

_.throttle(func, [wait=0], [options=])

引數

  1. func (Function): 要節流的函式。
  2. [wait=0] (number): 需要節流的毫秒。
  3. [options=] (Object): 選項物件。
  4. [options.leading=true] (boolean): 指定呼叫在節流開始前。
  5. [options.trailing=true] (boolean): 指定呼叫在節流結束後。

主方法如下:

function throttle(func, wait, options) {
  let leading = true
  let trailing = true
	// 首先判斷傳入的func是否是function型別
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }
	// 判斷配置引數是否是物件,並轉換options.leading的型別為boolean
  if (isObject(options)) {
    leading = 'leading' in options ? !!options.leading : leading
    trailing = 'trailing' in options ? !!options.trailing : trailing
  }
  // 執行debouce方法,並傳遞引數
  return debounce(func, wait, {
    leading,
    trailing,
    'maxWait': wait
  })
}

2、防抖:

_.debounce(func, [wait=0], [options=])#

引數

  1. func (Function): 要防抖動的函式。
  2. [wait=0] (number): 需要延遲的毫秒數。
  3. [options=] (Object): 選項物件。
  4. [options.leading=false] (boolean): 指定在延遲開始前呼叫。
  5. [options.maxWait] (number): 設定 func 允許被延遲的最大值。
  6. [options.trailing=true] (boolean): 指定在延遲結束後呼叫。

在lodash庫中,對於節流和防抖的處理,核心內容是debouce方法,下面對這個debounce方法進行簡單的分析

可配置化的debounce方法

function debounce(func, wait, options) {
  let lastArgs,
    lastThis,
    maxWait, // 最大等待時間
    result, // 執行func
    timerId, // 定時器控制代碼
    lastCallTime // 上次觸發的時間,比如不斷scroll,為上次scroll的時間

  let lastInvokeTime = 0 // 上次執行func的時間
  let leading = false // 配置引數,是否第一次觸發立即執行
  let maxing = false // 是否有最長等待時間
  let trailing = true // 是否在等待週期結束後執行傳入的func函式

  // 如果wait沒傳,呼叫window.requestAnimationFrame()
  const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
	// 判斷func是否是函式型別
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }

	// 從傳入的options中取出引數並做一些型別轉換
  wait = +wait || 0
  if (isObject(options)) {
    leading = !!options.leading
    maxing = 'maxWait' in options
    maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
    trailing = 'trailing' in options ? !!options.trailing : trailing
  }

	// 定義函式invokeFunc:使用者傳入的func方法的執行函式,傳入引數time,並更新lastInvokeTime記錄上次執行invokeFunc的時間
  function invokeFunc(time) {
    const args = lastArgs
    const thisArg = lastThis

    lastArgs = lastThis = undefined
    lastInvokeTime = time
    result = func.apply(thisArg, args)
    return result
  }

	// 定義函式startTimer: 建立一個定時器,傳參pendingFunc待執行函式,wait延遲多久後執行
  function startTimer(pendingFunc, wait) {
    if (useRAF) {
      root.cancelAnimationFrame(timerId)
      return root.requestAnimationFrame(pendingFunc)
    }
    return setTimeout(pendingFunc, wait)
  }

	// 定義函式cancelTimer: 清除建立的這個定時器:id控制代碼
  function cancelTimer(id) {
    if (useRAF) {
      return root.cancelAnimationFrame(id)
    }
    clearTimeout(id)
  }

  // 定義leadingEdge:防抖開始執行的函式,根據leading判斷,如果true,立即執行
  function leadingEdge(time) {
    // 開始執行,則記錄這個time為上次執行func的時間:lastInvokeTime
    lastInvokeTime = time
    // 開始建立定時器執行
    timerId = startTimer(timerExpired, wait)
    // 根據leading引數判斷是否立即執行
    return leading ? invokeFunc(time) : result
  }

	// 根據傳入的time,計算還需要等待的時間
  function remainingWait(time) {
    const timeSinceLastCall = time - lastCallTime // 現在距離上次觸發scroll的時間
    const timeSinceLastInvoke = time - lastInvokeTime // 現在距離上次執行func的時間
    const timeWaiting = wait - timeSinceLastCall // wait延遲時間 - 距離上次觸發scroll的時間
    return maxing
      ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting 
		// 如果設定了最大等待時間,則還需等待:(延遲時間wait - 已經等候時間,最大等待時間-上次執行func剩餘的時間)兩者取較小值
		// 否則,還需等待 wait - 已經等候時間
  }

// 根據傳入的time判斷是否應該執行func函式
  function shouldInvoke(time) {
    const timeSinceLastCall = time - lastCallTime // 上次觸發
    const timeSinceLastInvoke = time - lastInvokeTime // 上次執行

   // 四種情況執行:
			// 1、第一次觸發,lastCallTime為undefined
			// 2、距離上次觸發已經大於延遲時間了
			// 3、當前-上次觸發<0,特殊情況,比如原本是2020,修改了系統時間為2018
			// 4、距離上次執行的時間> 最長等待時間了
    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
  }

	// 防抖核心,判斷是執行函式,還是繼續設定定時器
  function timerExpired() {
    const time = Date.now()
    if (shouldInvoke(time)) { // 根據當前時間,判斷是否應該執行,如果是,執行func
      return trailingEdge(time)
    }
    // 否則,重置定時器,將剩餘的時間傳入
    timerId = startTimer(timerExpired, remainingWait(time))
  }

	// 執行func的判斷函式
  function trailingEdge(time) {
    timerId = undefined

    // Only invoke if we have `lastArgs` which means `func` has been
    // debounced at least once.
    if (trailing && lastArgs) {
      return invokeFunc(time)
    }
    lastArgs = lastThis = undefined
    return result
  }

// 取消防抖
  function cancel() {
    if (timerId !== undefined) {
      cancelTimer(timerId)
    }
    lastInvokeTime = 0
    lastArgs = lastCallTime = lastThis = timerId = undefined
  }
  // 直接執行
  function flush() {
    return timerId === undefined ? result : trailingEdge(Date.now())
  }

	// 判斷是否在等待中
  function pending() {
    return timerId !== undefined
  }

// 入口函式
  function debounced(...args) {
    const time = Date.now()
    const isInvoking = shouldInvoke(time) // 根據當前時間判斷是否應該執行func函式

    lastArgs = args
    lastThis = this
    lastCallTime = time

    if (isInvoking) {
			// 如果定時器還未建立,建立定時器按照所設定的是否立即執行去執行
      if (timerId === undefined) {
        return leadingEdge(lastCallTime)
      }
			// 如果設定了最長等待時間,建立定時器,返回func的執行方法
      if (maxing) {
        // Handle invocations in a tight loop.
        timerId = startTimer(timerExpired, wait)
        return invokeFunc(lastCallTime)
      }
    }
		// 如果還沒有建立定時器,建立定時器
    if (timerId === undefined) {
      timerId = startTimer(timerExpired, wait)
    }
    return result
  }
  debounced.cancel = cancel
  debounced.flush = flush
  debounced.pending = pending
  return debounced
}

export default debounce

基於以上debounce的基礎,其實lodash中,對節流的的實現,就是傳了一個maxWait引數( func 允許被延遲的最大值)為wait (延遲數),它的結果是如果連續不斷觸發則每隔 wait 秒執行一次func。

參考文章:防抖和節流概念理解—timeline圖lodash原始碼賞析