LeoWangJ / blog

紀錄學習文章
1 stars 0 forks source link

節流(throttle) 實現 #12

Open LeoWangJ opened 5 years ago

LeoWangJ commented 5 years ago

節流

持續觸發某個任務, 每隔一段時間觸發一次。

簡單來說 就是多久執行一次

立即執行

開始執行時會先呼叫一次, 接下來依照時間戳來判斷是否重新觸發。

 throttle (fn, wait) {
    var prev = 0
    return function () {
      var now = +new Date()
      if (now - prev > wait) {
        fn.apply(this, arguments)
        prev = now
      }
    }
  }

非立即執行

經過設定的時間數後才會執行,我們透過setTimeout來判斷隔了多久觸發一次。

throttle (fn, wait) {
    var timeout
    return function () {
      var args = arguments
      if (timeout) {
        return false
      }

      timeout = setTimeout(() => {
        fn.apply(this, args)
        timeout = null
      }, wait)
    }
  }

立即執行與非立即執行區別

立即執行: 執行任務時會先執行一次,但當事件停止觸發時任務就會立即停止。 例如: 在輸入註冊帳號時,設置每秒執行一次驗證註冊帳號是否符合資格,若在第2.5秒停止輸入這時只會執行兩次驗證。

非立即執行: 執行任務時會先等到指定時間到才會執行,但當事件停止觸發時依舊還會執行最後一次事件。 意思為2.5秒停止輸入等到第三秒時還會再執行一次驗證

整合兩種方式的優點

我們想要實現出可以立即執行並且在停止輸入時還會觸發最後一次事件的方法

throttle (fn, wait) {
    var timeout, remaining, context, args
    var prev = 0
    // 執行最後一次事件
    var later = function () {
      prev = +new Date()
      timeout = null
      fn.apply(context, args)
      console.log('最後執行')
    }

    var throttled = function () {
      context = this
      args = arguments
      var now = +new Date()
      // 下次觸發任務(fn)的剩餘時間
      remaining = wait - (now - prev)
      // 剩餘時間小於0,代表可以觸發事件
      if (remaining <= 0) {
        // 確保下次還會進入setTimeout
        if (timeout) {
          clearTimeout(timeout)
          timeout = null
        }
        prev = now
        fn.apply(context, args)
      } else if (!timeout) {
        timeout = setTimeout(later, remaining)
      }
    }

    return throttled
  }

優化

有時候並不想要上面的版本, 只想要原先的非立即執行的版本, 為了兼容這個問題我們想到了使用參數來控制想要使用哪種版本。 我們使用options作為第三個參數,而options中有兩個key:

  1. leading:false 表示禁用第一次執行
  2. trailing: false 表示禁用停止觸發的回調
throttle (fn, wait, options) {
    var timeout, remaining, context, args
    var prev = 0

    if (!options) options = {}
    // 執行最後一次事件
    var later = function () {
      prev = options.leading === false ? 0 : +new Date()
      timeout = null
      fn.apply(context, args)
      console.log('最後執行')
    }

    var throttled = function () {
      context = this
      args = arguments
      var now = +new Date()
      if (!prev && options.leading === false) prev = now
      // 下次觸發任務(fn)的剩餘時間
      remaining = wait - (now - prev)
      // 剩餘時間小於0,代表可以觸發事件
      if (remaining <= 0) {
        // 確保下次還會進入setTimeout
        if (timeout) {
          clearTimeout(timeout)
          timeout = null
        }
        prev = now
        fn.apply(context, args)
      } else if (!timeout && options.trailing !== false) {
        timeout = setTimeout(later, remaining)
      }
    }

    return throttled
  }

回收變量

由於我們在閉包中儲存了許多變量都沒有回收, 所以在結束這個事件時順便回收閉包變量

throttle (fn, wait, options) {
    var timeout, remaining, context, args
    var prev = 0

    if (!options) options = {}
    // 執行最後一次事件
    var later = function () {
      prev = options.leading === false ? 0 : +new Date()
      timeout = null
      fn.apply(context, args)
      if (!timeout) context = args = null
      console.log('最後執行')
    }

    var throttled = function () {
      context = this
      args = arguments
      var now = +new Date()
      if (!prev && options.leading === false) prev = now
      // 下次觸發任務(fn)的剩餘時間
      remaining = wait - (now - prev)
      // 剩餘時間小於0,代表可以觸發事件
      if (remaining <= 0) {
        // 確保下次還會進入setTimeout
        if (timeout) {
          clearTimeout(timeout)
          timeout = null
        }
        prev = now
        fn.apply(context, args)
        if (!timeout) context = args = null
      } else if (!timeout && options.trailing !== false) {
        timeout = setTimeout(later, remaining)
      }
    }

    return throttled
  }

參考

从搜索系统来聊聊防抖和节流 JavaScript专题之跟着underscore学防抖