幫你徹底搞懂防抖和節流(附帶在React使用的一個例子)

1. 前言

開門見山,使用防抖節流技術的意義:節約資源,提升用戶體驗

瀏覽器中有許多事件會在很小時間間隔內頻繁觸發,比如:監聽用戶的輸入(keyup、keydown)、瀏覽器窗口調整大小和滾動行爲(resize、scroll)、鼠標的移動行爲(mousemove)等。如果這些事件一觸發我們就執行相應的事件處理函數的話,那將會造成較大的資源浪費或者給用戶帶來不好的體驗。
例如,我們爲輸入框綁定keyup回調函數,判斷用戶輸入的手機號是否符合規則,在普通情況下,用戶每輸入一個字符,我們就會對當前的輸入內容進行判斷並給出相應的提示,但實際上用戶的輸入並未結束,我們就在頁面中給出輸入不符合規則的錯誤提示,這樣的用戶體驗很不好,並且這麼頻繁的驗證沒必要。實際中我們可能採用的處理方式是:當用戶在停止輸入一段時間後我們再判斷輸入的內容是否符合規則。(後面我們使用React來實現該例子)

這時,我們的防抖節流兩位小兄弟就排上用場了!

2. 防抖(debounce)

防抖:觸發高頻事件後n秒內函數只會執行一次,如果n秒內高頻事件再次被觸發,則重新計算時間。
思路:每次觸發事件時都取消之前的延時調用方法。
一種實現方式如下:

function debounce(fn, ms) {
	let timeout = null; // 創建一個標記用來存放定時器的返回值
	return function () {
		clearTimeout(timeout); // 每當用戶輸入的時候把前一個 setTimeout clear 掉
		// 然後又創建一個新的 setTimeout, 這樣就能保證輸入字符後的 interval 間隔內如果還有字符輸入的話,就不會執行 fn 函數
		timeout = setTimeout(() => { 
			fn.apply(this, arguments);
		}, ms);
	};
}


// 使用例子
function sayHi() {
	console.log('防抖成功');
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi, 1000)); // 防抖

3. 節流(throttle)

節流: 高頻事件觸發,但在n秒內只會執行一次,所以節流會稀釋函數的執行頻率。
思路:每次觸發事件時都判斷當前是否有等待執行的延時函數。
一種實現方式如下:

function throttle(fn, ms) {
	let canRun = true; // 通過閉包保存一個標記
	return function () {
		if (!canRun) return; // 在函數開頭判斷標記是否爲true,不爲true則return
		canRun = false; // 立即設置爲false
		setTimeout(() => { // 將外部傳入的函數的執行放在setTimeout中
			fn.apply(this, arguments);
			/* 最後在setTimeout執行完畢後再把標記設置爲true(關鍵)表示可以執行下一次循環了。
			當定時器沒有執行的時候標記永遠是false,在開頭被return掉 */
			canRun = true;
		}, ms);
	};
}


// 使用例子
function sayHi(e) {
	console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi, 1000));

4. 兩個比喻來幫助區分防抖和節流

我們從詞本身的意義展開來解釋防抖和節流,希望能幫助大家更清楚地區分它們。

防抖:防止抖動的意思,也就是不抖動時才進行相應的處理。比如一根拉直的彈簧,我們撥動一下它就會抖動,過一段時間後彈簧會恢復到平靜的狀態(從撥動彈簧使其抖動到恢復平靜的時長就是代碼例子的ms值)。在這個過程中,撥動彈簧的這一行爲假設爲事件被觸發(代碼中的input事件被觸發),當彈簧恢復平靜時我們再執行事件處理函數(代碼中的sayHi函數)。基於以上假設,當我們在彈簧還沒恢復到平靜狀態時,又不斷地撥動它(清除了原來的setTimeout,並重新開始計時),因爲彈簧還沒恢復到平靜,那麼事件處理函數就一直不會被執行。只有當我們撥動它,並且之後再也不動它(也就是最後一次觸發),等它恢復到平靜狀態時(setTimeout時間到達),事件處理函數纔會被執行。

節流:控制住流量的意思,流量沒達到一定的程度就不進行相應的處理。比如我們用水桶去接水,水龍頭保持以不變的流量出水(即事件不斷被觸發),只有當水桶裏的水滿的時候(setTimeout時間到達),我們纔將裝滿水的水桶拿走(執行事件處理函數),使用完後再拿這個空桶繼續接水(重新開始計時)。

從以上的比喻中我們可以知道,防抖是用來處理那些離散的事件(撥動彈簧),節流是用來處理那些連續的事件(水一直在流出),這樣我們就可以根據事件觸發是離散型的還是連續型的來判斷使用防抖還是節流啦(當然還要考慮實際需求)!
防抖應用的例子:判斷用戶的輸入情況,只在用戶停止輸入一段時間後再進行判斷。
節流應用的例子:滾動頁面垂直滾動條,判斷是否滾動到頁面底部。

5. 在React中使用

5.1 未使用防抖

import * as React from 'react'
import './App.css'

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      tip: null,
      trigerTimes: 1
    }
  }
  handleKeyUp = (e) => {
    this.isPhoneLegal(e.target.value) // 對用戶輸入進行判斷
  }

  isPhoneLegal = (phone) => {
    const phoneRegexp = /^1([38]\d|5[0-35-9]|7[3678])\d{8}$/  //手機號碼的正則表達式
    const { trigerTimes } = this.state
    if(phoneRegexp.test(phone)) {
      this.setState({
        tip: `手機號符合規則!`,
        trigerTimes: 0
      })
    } else {
      this.setState({
        tip: `手機號有誤, 觸發了:${trigerTimes}次`,
        trigerTimes: trigerTimes + 1
      })
    }
  }

  render() {
    return (
      <div className="container">
        <input onKeyUp={ this.handleKeyUp } placeholder="請輸入手機號"/>
        <span>
          {this.state.tip}
        </span>
      </div>
    )
  }
}

export default App

運行上述代碼,得到的結果如下:
未使用防抖
可以看到,我們每輸入一個字符,keyup事件就被觸發一次,用戶未輸入完成就提示輸入有誤,這種體驗不是很好。

5.2 使用防抖

import * as React from 'react'
import './App.css'

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      tip: null,
      trigerTimes: 1
    }
    this.isPhoneLegal = debounce(this.isPhoneLegal, 1000)
  }
  handleKeyUp = (e) => {
    this.isPhoneLegal(e.target.value) // 對用戶輸入進行判斷
  }
  isPhoneLegal = (phone) => {
    const phoneRegexp = /^1([38]\d|5[0-35-9]|7[3678])\d{8}$/
    const { trigerTimes } = this.state
    if(phoneRegexp.test(phone)) {
      this.setState({
        tip: `手機號符合規則!`,
        trigerTimes: 0
      })
    } else {
      this.setState({
        tip: `手機號有誤, 觸發了:${trigerTimes}次`,
        trigerTimes: trigerTimes + 1
      })
    }
  }

  render() {
    return (
      <div className="container">
        <input onKeyUp={ this.handleKeyUp} placeholder="請輸入手機號"/>
        <span>
          {this.state.tip}
        </span>
      </div>
    )
  }
}

function debounce(fn, ms) {
  let timeoutId
  return function () {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => {
      fn.apply(this, arguments)
    }, ms)
  }
}

export default App

運行上面代碼,得到如下結果:
使用防抖
結果顯示,此時並不會在用戶每次輸入字符的時候都進行規則判斷。我們在輸入到第10位時停頓了一下,然後才執行判斷,輸出手機號不符合規則的信息。很明顯,使用防抖以後,回調執行的次數大大減少了,這樣有利於節約資源,提升用戶體驗。假設這是一個判斷用戶名是否存在的輸入框,那麼我們的事件觸發回調就需要進行Ajax請求,後端查詢數據庫判斷用戶名是否存在,此時,減少回調函數的調用次數可以大大減少網絡請求數,降低服務器的壓力。

怎麼樣,學會了防抖和節流了嗎?

文中部分內容參考自這些文章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章