javascript函數防抖Debounce

1、函數爲何要防抖

有以下代碼數組

window.onresize = () => {
  console.log('觸發窗口監聽回調函數')
}
複製代碼複製代碼

當咱們在PC上縮放瀏覽器窗口時,一秒能夠輕鬆觸發30次事件。手機端觸發其餘Dom時間監聽回調時同理。瀏覽器

這裏的回調函數只是打印字符串,若是回調函數更加複雜,可想而知瀏覽器的壓力會很是大,用戶體驗會很糟糕。bash

resizescroll等Dom事件的監聽回調會被頻繁觸發,所以咱們要對其進行限制。app

2、實現思路

函數去抖簡單來講就是對於必定時間段的連續的函數調用,只讓其執行一次,初步的實現思路以下:異步

第一次調用函數,建立一個定時器,在指定的時間間隔以後運行代碼。當第二次調用該函數時,它會清除前一次的定時器並設置另外一個。若是前一個定時器已經執行過了,這個操做就沒有任何意義。然而,若是前一個定時器還沒有執行,其實就是將其替換爲一個新的定時器。目的是隻有在執行函數的請求中止了一段時間以後才執行。async

3、Debounce 應用場景

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

4、函數防抖最終版

代碼說話,有錯懇請指出ide

function debounce(method, wait, immediate) {
  let timeout
  // debounced函數爲返回值
  // 使用Async/Await處理異步,若是函數異步執行,等待setTimeout執行完,拿到原函數返回值後將其返回
  // args爲返回函數調用時傳入的參數,傳給method
  let debounced = function(...args) {
    return new Promise (resolve => {
      // 用於記錄原函數執行結果
      let result
      // 將method執行時this的指向設爲debounce返回的函數被調用時的this指向
      let context = this
      // 若是存在定時器則將其清除
      if (timeout) {
        clearTimeout(timeout)
      }
      // 當即執行須要兩個條件,一是immediate爲true,二是timeout未被賦值或被置爲null
      if (immediate) {
        // 若是定時器不存在,則當即執行,並設置一個定時器,wait毫秒後將定時器置爲null
        // 這樣確保當即執行後wait毫秒內不會被再次觸發
        let callNow = !timeout
        timeout = setTimeout(() => {
          timeout = null
        }, wait)
        // 若是知足上述兩個條件,則當即執行並記錄其執行結果
        if (callNow) {
          result = method.apply(context, args)
          resolve(result)
        }
      } else {
        // 若是immediate爲false,則等待函數執行並記錄其執行結果
        // 並將Promise狀態置爲fullfilled,以使函數繼續執行
        timeout = setTimeout(() => {
          // args是一個數組,因此使用fn.apply
          // 也可寫做method.call(context, ...args)
          result = method.apply(context, args)
          resolve(result)
        }, wait)
      }
    })
  }

  // 在返回的debounced函數上添加取消方法
  debounced.cancel = function() {
    clearTimeout(timeout)
    timeout = null
  }

  return debounced
}
複製代碼複製代碼

須要注意的是,若是須要原函數返回值,調用防抖後的函數的外層函數須要使用Async/Await語法等待執行結果返回函數

使用方法見代碼:優化

function square(num) {
  return Math.pow(num, 2)
}

let debouncedFn = debounce(square, 1000, false)

window.addEventListener('resize', async () => {
  let val
  try {
    val = await debouncedFn(4)
  } catch (err) {
    console.error(err)
  }
  // 中止縮放1S後輸出:
  // 原函數的返回值爲:16
  console.log(`原函數返回值爲${val}`)
}, false)
複製代碼複製代碼

具體的實現步驟請往下看ui

5、Debounce 的實現

1. 《JavaScript高級程序設計》(第三版)中的實現

function debounce(method, context) {
  clearTimeout(method.tId)
  method.tId = setTimeout(() => {
    method.call(context)
  }, 1000)
}

function print() {
  console.log('Hello World')
}

window.onresize = debounce(print)
複製代碼複製代碼

咱們不停縮放窗口,當中止1S後,打印出Hello World。

有個能夠優化的地方: 此實現方法有反作用(Side Effect),改變了輸入值(method),給method新增了屬性

2. 優化初版:消除反作用,將定時器隔離

function debounce(method, wait, context) {
  let timeout
  return function() {
    if (timeout) {
      clearTimeout(timeout)
    }
    timeout = setTimeout(() => {
      method.call(context)
    }, wait)
  }
}
複製代碼複製代碼

3. 優化第二版:自動調整this正確指向

以前的函數咱們須要手動傳入函數執行上下文context,如今優化將 this 指向正確的對象。

function debounce(method, wait) {
  let timeout
  return function() {
    // 將method執行時this的指向設爲debounce返回的函數被調用時的this指向
    let context = this
    if (timeout) {
      clearTimeout(timeout)
    }
    timeout = setTimeout(() => {
      method.call(context)
    }, wait)
  }
}
複製代碼複製代碼

4. 優化第三版:函數可傳入參數

即使咱們的函數不須要傳參,可是別忘了JavaScript 在事件處理函數中會提供事件對象 event,因此咱們要實現傳參功能。

function debounce(method, wait) {
  let timeout
  // args爲返回函數調用時傳入的參數,傳給method
  return function(...args) {
    let context = this
    if (timeout) {
      clearTimeout(timeout)
    }
    timeout = setTimeout(() => {
      // args是一個數組,因此使用fn.apply
      // 也可寫做method.call(context, ...args)
      method.apply(context, args)
    }, wait)
  }
}
複製代碼複製代碼

5. 優化第四版:提供當即執行選項

有些時候我不但願非要等到事件中止觸發後才執行,我但願馬上執行函數,而後等到中止觸發n毫秒後,才能夠從新觸發執行。

function debounce(method, wait, immediate) {
  let timeout
  return function(...args) {
    let context = this
    if (timeout) {
      clearTimeout(timeout)
    }
    // 當即執行須要兩個條件,一是immediate爲true,二是timeout未被賦值或被置爲null
    if (immediate) {
      // 若是定時器不存在,則當即執行,並設置一個定時器,wait毫秒後將定時器置爲null
      // 這樣確保當即執行後wait毫秒內不會被再次觸發
      let callNow = !timeout
      timeout = setTimeout(() => {
        timeout = null
      }, wait)
      if (callNow) {
        method.apply(context, args)
      }
    } else {
      // 若是immediate爲false,則函數wait毫秒後執行
      timeout = setTimeout(() => {
        // args是一個類數組對象,因此使用fn.apply
        // 也可寫做method.call(context, ...args)
        method.apply(context, args)
      }, wait)
    }
  }
}
複製代碼複製代碼

6. 優化第五版:提供取消功能

有些時候咱們須要在不可觸發的這段時間內可以手動取消防抖,代碼實現以下:

function debounce(method, wait, immediate) {
  let timeout
  // 將返回的匿名函數賦值給debounced,以便在其上添加取消方法
  let debounced = function(...args) {
    let context = this
    if (timeout) {
      clearTimeout(timeout)
    }
    if (immediate) {
      let callNow = !timeout
      timeout = setTimeout(() => {
        timeout = null
      }, wait)
      if (callNow) {
        method.apply(context, args)
      }
    } else {
      timeout = setTimeout(() => {
        method.apply(context, args)
      }, wait)
    }
  }

  // 加入取消功能,使用方法以下
  // let myFn = debounce(otherFn)
  // myFn.cancel()
  debounced.cancel = function() {
    clearTimeout(timeout)
    timeout = null
  }
}
複製代碼複製代碼

至此,咱們已經比較完整地實現了一個underscore中的debounce函數。

6、遺留問題

須要防抖的函數多是存在返回值的,咱們要對這種狀況進行處理,underscore的處理方法是將函數返回值在返回的debounced函數內再次返回,可是這樣實際上是有問題的。若是參數immediate傳入值不爲true的話,當防抖後的函數第一次被觸發時,若是原始函數有返回值,實際上是拿不到返回值的,由於原函數是在setTimeout內,是異步延遲執行的,而return是同步執行的,因此返回值是undefined

第二次觸發時拿到的返回值實際上是第一次執行的返回值,第三次觸發時拿到的返回值實際上是第二次執行的返回值,以此類推。

1. 使用回調函數處理函數返回值

function debounce(method, wait, immediate, callback) {
  let timeout, result
  let debounced = function(...args) {
    let context = this
    if (timeout) {
      clearTimeout(timeout)
    }
    if (immediate) {
      let callNow = !timeout
      timeout = setTimeout(() => {
        timeout = null
      }, wait)
      if (callNow) {
        result = method.apply(context, args)
        // 使用回調函數處理函數返回值
        callback && callback(result)
      }
    } else {
      timeout = setTimeout(() => {
        result = method.apply(context, args)
        // 使用回調函數處理函數返回值
        callback && callback(result)
      }, wait)
    }
  }

  debounced.cancel = function() {
    clearTimeout(timeout)
    timeout = null
  }

  return debounced
}
複製代碼複製代碼

這樣咱們就能夠在函數防抖時傳入一個回調函數來處理函數的返回值,使用代碼以下:

function square(num) {
  return Math.pow(num, 2)
}

let debouncedFn = debounce(square, 1000, false, val => {
  console.log(`原函數的返回值爲:${val}`)
})

window.addEventListener('resize', () => {
  debouncedFn(4)
}, false)

// 中止縮放1S後輸出:
// 原函數的返回值爲:16
複製代碼複製代碼

2. 使用Promise處理返回值

function debounce(method, wait, immediate) {
  let timeout, result
  let debounced = function(...args) {
    // 返回一個Promise,以即可以使用then或者Async/Await語法拿到原函數返回值
    return new Promise(resolve => {
      let context = this
      if (timeout) {
        clearTimeout(timeout)
      }
      if (immediate) {
        let callNow = !timeout
        timeout = setTimeout(() => {
          timeout = null
        }, wait)
        if (callNow) {
          result = method.apply(context, args)
          // 將原函數的返回值傳給resolve
          resolve(result)
        }
      } else {
        timeout = setTimeout(() => {
          result = method.apply(context, args)
          // 將原函數的返回值傳給resolve
          resolve(result)
        }, wait)
      }
    })
  }

  debounced.cancel = function() {
    clearTimeout(timeout)
    timeout = null
  }

  return debounced
}
複製代碼複製代碼

使用方法一:在調用防抖後的函數時,使用then拿到原函數的返回值

function square(num) {
  return Math.pow(num, 2)
}

let debouncedFn = debounce(square, 1000, false)

window.addEventListener('resize', () => {
  debouncedFn(4).then(val => {
    console.log(`原函數的返回值爲:${val}`)
  })
}, false)

// 中止縮放1S後輸出:
// 原函數的返回值爲:16
複製代碼複製代碼

使用方法二:調用防抖後的函數的外層函數使用Async/Await語法等待執行結果返回

使用方法見代碼:

function square(num) {
  return Math.pow(num, 2)
}

let debouncedFn = debounce(square, 1000, false)

window.addEventListener('resize', async () => {
  let val
  try {
    val = await debouncedFn(4)
  } catch (err) {
    console.error(err)
  }
  console.log(`原函數返回值爲${val}`)
}, false)

// 中止縮放1S後輸出:
// 原函數的返回值爲:16複製代碼
相關文章
相關標籤/搜索