防抖與節流 & 若每個請求必須發送,如何平滑地獲取最後一個介面返回的數據

  • 2019 年 11 月 2 日
  • 筆記

部落格地址:https://ainyi.com/79

日常瀏覽網頁中,在進行窗口的 resize、scroll 或者重複點擊某按鈕發送請求,此時事件處理函數或者介面調用的頻率若無限制,則會加重瀏覽器的負擔,介面可能顯示有誤,服務端也可能出問題,導致用戶體驗非常糟糕
此時可以採用 debounce(防抖)和 throttle(節流)的方式來減少事件或介面的調用頻率,同時又能實現預期效果

防抖:將幾次操作合併為一此操作進行。原理是維護一個計時器,規定在 delay 時間後觸發函數,但是在 delay 時間內再次觸發的話,就會取消之前的計時器而重新設置。這樣一來,只有最後一次操作能被觸發

節流:使得一定時間內只觸發一次函數。原理是通過判斷是否到達一定時間來觸發函數

區別: 函數節流不管事件觸發有多頻繁,都會保證在規定時間內一定會執行一次真正的事件處理函數,而函數防抖只是在連續觸發的事件後才觸發最後一次事件的函數

上面的解釋,摘抄網上的解答

防抖

debounce:當持續觸發事件時,一定時間段內沒有再觸發事件,事件處理函數才會執行一次,如果設定的時間到來之前,又一次觸發了事件,就重新開始延時

如下圖,持續觸發 scroll 事件時,並不執行 handle 函數,當 1000ms 內沒有觸發 scroll 事件時,才會延時觸發 scroll 事件
WechatIMG331.png

function debounce(fn, wait) {    let timeout = null    return function() {      if(timeout !== null) {        clearTimeout(timeout)      }      timeout = setTimeout(fn, wait)    }  }  // 處理函數  function handle() {    console.log('處理函數', Math.random())  }    // 滾動事件  window.addEventListener('scroll', debounce(handle, 1000))

節流

throttle:當持續觸發事件時,保證一定時間段內只調用一次事件處理函數
仔細了解了才知道,我以前剛學前端的時候,做 banner 圖特效,兩邊的點擊按鈕如果一直重複點擊就會出問題,後面摸索了此方法,原來這名字叫做節流

如下圖,持續觸發 scroll 事件時,並不立即執行 handle 函數,每隔 1000 毫秒才會執行一次 handle 函數
WechatIMG332.png

時間戳方法

let throttle = function(func, delay) {    let prev = Date.now()    return function() {      let context = this      let args = arguments      let now = Date.now()      if(now - prev >= delay) {        func.apply(context, args)        prev = Date.now()      }    }  }  function handle() {    console.log(Math.random())  }  window.addEventListener('scroll', throttle(handle, 1000))

定時器方法

let throttle = function(func, delay) {    let timer = null    return function() {      let context = this      let args = arguments      if(!timer) {        timer = setTimeout(function() {          func.apply(context, args)          timer = null        }, delay)      }    }  }  function handle() {    console.log(Math.random())  }  window.addEventListener('scroll', throttle(handle, 1000))

時間戳+定時器

let throttle = function(func, delay) {    let timer = null    let startTime = Date.now()    return function() {      let curTime = Date.now()      let remaining = delay - (curTime - startTime)      let context = this      let args = arguments      clearTimeout(timer)      if(remaining <= 0) {        func.apply(context, args)        startTime = Date.now()      } else {        timer = setTimeout(func, remaining)      }    }  }  function handle() {    console.log(Math.random())  }  window.addEventListener('scroll', throttle(handle, 1000))

每個請求必須發送的問題

如下圖的購買頁,操作發現一個購買明細的查價介面的頻繁調用問題
如下圖:
WechatIMG329.png

購買頁改變任何一個選項,都會調用查價介面,然後右邊會顯示對應的價格。尤其是購買數量,這是一個數字選擇器,如果用戶頻繁點擊 + 號,就會連續調用多次查價介面,但==最後一次的查價介面返回的數據才是最後選擇的正確的價格==
每個查價介面逐個請求完畢的時候,==右邊的顯示價格也會逐個改變==,最終變成最後正確的價格,一般來說,這是比較不友好的,用戶點了多次後,不想看到價格在變化,儘管最終是正確的價格,但這個變化的過程是不能接受的

也不應該使用上面的防抖解決方式,不能設置過長的定時器,因為查價介面不能等太久,也不能設置過短的定時器,否則會出現上面說的問題(價格在變化)

所以這是一個==每個請求必須發送,但是只顯示最後一個介面返回的數據的問題==

我這裡採用入棧、取棧頂元素比對請求參數的方法解決:

// 查價  async getPrice() {    // 請求參數    const reqData = this.handleData()    // push 入棧    this.priceStack.push(reqData)    const { result } = await getProductPrice(reqData)    // 核心程式碼,取棧頂元素(最後請求的參數)比對    if(this.$lang.isEqual(this.$array.last(this.priceStack), reqData)) {      // TODO      // 展示價格程式碼...    }  }

註解,上述的 this.$lang.isEqual、this.$array.last 均是 lodash 插件提供的方法
註冊到 Vue 中

import array from 'lodash/array'  import Lang from 'lodash/lang'    Vue.prototype.$array = array  Vue.prototype.$lang = Lang

部落格地址:https://ainyi.com/79