【進階 7-3 期】[譯] Throttle 和 Debounce 在 React 中的應用

更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看css

------ 如下是正文 ------前端

原文 Improve Your React App Performance by Using Throttling and Debouncingreact

引言

使用 React 構建應用程序時,咱們老是會遇到一些限制問題,好比大量的調用、異步網絡請求和 DOM 更新等,咱們可使用 React 提供的功能來檢查這些。git

  • shouldComponentUpdate(...) 生命週期鉤子
  • React.PureComponent
  • React.memo
  • Windowing and Virtualization
  • Memoization
  • Hydration
  • Hooks (useState, useMemo, useContext, useReducer, 等)

在這篇文章中,咱們將研究如何在不使用 React 提供的功能下來改進 React 應用程序性能,咱們將使用一種不只僅適用於 React 的技術:節流(Throttle)和防抖(Debounce)。github

從一個例子開始

例子 1

下面這個例子能夠很好的解釋節流和防抖帶給咱們的好處,假設咱們有一個 autocomp 組件面試

import React from 'react';
import './autocomp.css';
複製代碼
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state= {
            results: []
        }
    }
複製代碼
handleInput = evt => {
        const value = evt.target.value
        fetch(`/api/users`)
            .then(res => res.json())
            .then(result => this.setState({ results: result.users }))
    }
複製代碼
render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'> <input placeholder="Enter your search.." onChange={this.handleInput} /> <div> {results.map(item=>{item})} </div> </div> ); } } export default autocomp; 複製代碼

在咱們的 autocomp 組件中,一旦咱們在輸入框中輸入一個單詞,它就會請求 api/users 獲取要顯示的用戶列表。 在每一個字母輸入後,觸發異步網絡請求,而且成功後經過 this.setState 更新DOM。數據庫

如今,想象一下輸入 fidudusola 嘗試搜索結果 fidudusolanke,將有許多名稱與 fidudusola 一塊兒出現。npm

1.  f
2.  fi
3.  fid
4.  fidu
5.  fidud
6.  fidudu
7.  fidudus
8.  fiduduso
9.  fidudusol
10. fidudusola
複製代碼

這個名字有 10 個字母,因此咱們將有 10 次 API 請求和 10 次 DOM 更新,這只是一個用戶而已!! 輸入完成後最終看到咱們預期的名字 fidudusolanke 和其餘結果一塊兒出現。json

即便 autocomp 能夠在沒有網絡請求的狀況下完成(例如,內存中有一個本地「數據庫」),仍然須要爲輸入的每一個字符/單詞進行昂貴的 DOM 更新。api

const data = [
    {
        name: 'nnamdi'
    },
    {
        name: 'fidudusola'
    },
    {
        name: 'fashola'
    },
    {
        name: 'fidudusolanke'
    },
    // ... up to 10,000 records
]
複製代碼
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state= {
            results: []
        }
    }
複製代碼
handleInput = evt => {
        const value = evt.target.value
        const filteredRes = data.filter((item)=> {
            // algorithm to search through the `data` array
        })
        this.setState({ results: filteredRes })
    }
複製代碼
render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'> <input placeholder="Enter your search.." onChange={this.handleInput} /> <div> {results.map(result=>{result})} </div> </div> ); } } 複製代碼

例子 2

另外一個例子是使用 resizescroll 等事件。大多數狀況下,網站每秒滾動 1000 次,想象一下在 scroll 事件中添加一個事件處理。

document.body.addEventListener('scroll', ()=> {
    console.log('Scrolled !!!')
})
複製代碼

你會發現這個函數每秒被執行 1000 次!若是這個事件處理函數執行大量計算或大量 DOM 操做,將面臨最壞的狀況。

function longOp(ms) {
    var now = Date.now()
    var end = now + ms
    while(now < end) {
        now = Date.now()
    }
}
複製代碼
document.body.addEventListener('scroll', ()=> {
    // simulating a heavy operation
    longOp(9000)
    console.log('Scrolled !!!')
})
複製代碼

咱們有一個須要 9 秒才能完成的操做,最後輸出 Scrolled !!!,假設咱們滾動 5000 像素會有 200 多個事件被觸發。 所以,須要 9 秒才能完成一次的事件,大約須要 9 * 200 = 1800s 來運行所有的 200 個事件。 所以,所有完成須要 30 分鐘(半小時)。

因此確定會發現一個滯後且無響應的瀏覽器,所以編寫的事件處理函數最好在較短的時間內執行完成。

咱們發現這會在咱們的應用程序中產生巨大的性能瓶頸,咱們不須要在輸入的每一個字母上執行 API 請求和 DOM 更新,咱們須要等到用戶中止輸入或者輸入一段時間以後,等到用戶中止滾動或者滾動一段時間以後,再去執行事件處理函數。

全部這些確保咱們的應用程序有良好性能,讓咱們看看如何使用節流和防抖來避免這種性能瓶頸。

節流 Throttle

節流強制一個函數在一段時間內能夠調用的最大次數,例如每 100 毫秒最多執行一次函數。

節流是指在指定的時間內執行一次給定的函數。這限制了函數被調用的次數,因此重複的函數調用不會重置任何數據。

假設咱們一般以 1000 次 / 20 秒的速度調用函數。 若是咱們使用節流將它限制爲每 500 毫秒執行一次,咱們會看到函數在 20 秒內將執行 40 次。

1000 * 20 secs = 20,000ms
20,000ms / 500ms = 40 times
複製代碼

這是從 1000 次到 40 次的極大優化。

下面將介紹在 React 中使用節流的例子,將分別使用 underscorelodashRxJS 以及自定義實現。

使用 underscore

咱們將使用 underscore 提供的節流函數處理咱們的 autocomp 組件。

先安裝依賴。

npm i underscore
複製代碼

而後在組件中導入它:

// ...
import * as _ from underscore;
複製代碼
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []
        }
       this.handleInputThrottled = _.throttle(this.handleInput, 1000)
    }
複製代碼
handleInput = evt => {
        const value = evt.target.value
        const filteredRes = data.filter((item)=> {
            // algorithm to search through the `data` array
        })
        this.setState({ results: filteredRes })
    }
複製代碼
render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'> <input placeholder="Enter your search.." onChange={this.handleInputThrottled} /> <div> {results.map(result=>{result})} </div> </div> ); } } 複製代碼

節流函數接收兩個參數,分別是須要被限制的函數和時間差,返回一個節流處理後的函數。 在咱們的例子中,handleInput 方法被傳遞給 throttle 函數,時間差爲 1000ms。

如今,假設咱們以每 200ms 1 個字母的正常速度輸入 fidudusola,輸入完成須要10 * 200ms =(2000ms)2s,這時 handleInput 方法將只調用 2(2000ms / 1000ms = 2)次而不是最初的 10 次。

使用 lodash

lodash 也提供了一個 throttle 函數,咱們能夠在 JS 程序中使用它。

首先,咱們須要安裝依賴。

npm i lodash
複製代碼

使用 lodash,咱們的 autocomp 將是這樣的。

// ...
import { throttle } from lodash;
複製代碼
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []
        }
       this.handleInputThrottled = throttle(this.handleInput, 100)
    }
複製代碼
handleInput = evt => {
        const value = evt.target.value
        const filteredRes = data.filter((item)=> {
            // algorithm to search through the `data` array
        })
        this.setState({ results: filteredRes })
    }
複製代碼
render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'> <input placeholder="Enter your search.." onChange={this.handleInputThrottled} /> <div> {results.map(result=>{result})} </div> </div> ); } } 複製代碼

underscore 同樣的效果,沒有其餘區別。

使用 RxJS

JS 中的 Reactive Extensions 提供了一個節流運算符,咱們可使用它來實現功能。

首先,咱們安裝 rxjs

npm i rxjs
複製代碼

咱們從 rxjs 庫導入 throttle

// ...
import { BehaviorSubject } from 'rxjs';
import { throttle } from 'rxjs/operators';
複製代碼
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []
        }
        this.inputStream = new BehaviorSubject()
    }
複製代碼
componentDidMount() {
        this.inputStream
            .pipe(
                throttle(1000)
            )
            .subscribe(v => {
                const filteredRes = data.filter((item)=> {
                    // algorithm to search through the `data` array
                })
                this.setState({ results: filteredRes })
        })
    }

複製代碼
render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'> <input placeholder="Enter your search.." onChange={e => this.inputStream.next(e.target.value)} /> <div> {results.map(result => { result })} </div> </div> ); } } 複製代碼

咱們從 rxjs 中導入了 throttleBehaviorSubject,初始化了一個 BehaviorSubject 實例保存在 inputStream 屬性,在 componentDidMount 中,咱們將 inputStream 流傳遞給節流操做符,傳入 1000,表示 RxJS 節流控制爲 1000ms,節流操做返回的流被訂閱以得到流值。

由於在組件加載時訂閱了 inputStream,因此咱們開始輸入時,輸入的內容就被髮送到 inputStream 流中。 剛開始時,因爲 throttle 操做符 1000ms 內不會發送內容,在這以後發送最新值, 發送以後就開始計算獲得結果。

若是咱們以 200ms 1 個字母的速度輸入 fidudusola ,該組件將從新渲染 2000ms / 1000ms = 2次。

使用自定義實現

咱們實現本身的節流函數,方便更好的理解節流如何工做。

咱們知道在一個節流控制的函數中,它會根據指定的時間間隔調用,咱們將使用 setTimeout 函數實現這一點。

function throttle(fn, ms) {
    let timeout
    function exec() {
        fn.apply()
    }
    function clear() {
        timeout == undefined ? null : clearTimeout(timeout)
    }
    if(fn !== undefined && ms !== undefined) {
        timeout = setTimeout(exec, ms)
    } else {
        console.error('callback function and the timeout must be supplied')
    }
    // API to clear the timeout
    throttle.clearTimeout = function() {
        clear();
    }
}
複製代碼

注:原文自定義實現的節流函數有問題,節流函數的詳細實現和解析能夠查看個人另外一篇文章,點擊查看

個人實現以下:

// fn 是須要執行的函數
// wait 是時間間隔
const throttle = (fn, wait = 50) => {
  // 上一次執行 fn 的時間
  let previous = 0
  // 將 throttle 處理結果看成函數返回
  return function(...args) {
    // 獲取當前時間,轉換成時間戳,單位毫秒
    let now = +new Date()
    // 將當前時間和上一次執行函數的時間進行對比
    // 大於等待時間就把 previous 設置爲當前時間並執行函數 fn
    if (now - previous > wait) {
      previous = now
      fn.apply(this, args)
    }
  }
}
複製代碼

上面的實現很是簡單,在 React 項目中使用方式以下。

// ...
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []
        }
       this.handleInputThrottled = throttle(this.handleInput, 100)
    }
複製代碼
handleInput = evt => {
        const value = evt.target.value
        const filteredRes = data.filter((item)=> {
            // algorithm to search through the `data` array
        })
        this.setState({ results: filteredRes })
    }
複製代碼
render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'> <input placeholder="Enter your search.." onChange={this.handleInputThrottled} /> <div> {results.map(result=>{result})} </div> </div> ); } } 複製代碼

防抖 Debounce

防抖會強制自上次調用後通過必定時間纔會再次調用函數,例如只有在沒有被調用的狀況下通過一段時間以後(例如100毫秒)才執行該函數。

在防抖時,它忽略對函數的全部調用,直到函數中止調用一段時間以後纔會再次執行。

下面將介紹在項目中使用 debounce 的例子。

使用 underscore

// ...
import * as _ from 'underscore';
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []
        }
       this.handleInputThrottled = _.debounce(this.handleInput, 100)
    }
    handleInput = evt => {
        const value = evt.target.value
        const filteredRes = data.filter((item)=> {
            // algorithm to search through the `data` array
        })
        this.setState({ results: filteredRes })
    }
    render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'> <input placeholder="Enter your search.." onChange={this.handleInputThrottled} /> <div> {results.map(result=>{result})} </div> </div> ); } } 複製代碼

使用 lodash

// ...
import { debounce } from 'lodash';
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []
        }
       this.handleInputThrottled = debounce(this.handleInput, 100)
    }
    handleInput = evt => {
        const value = evt.target.value
        const filteredRes = data.filter((item)=> {
            // algorithm to search through the `data` array
        })
        this.setState({ results: filteredRes })
    }
    render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'> <input placeholder="Enter your search.." onChange={this.handleInputThrottled} /> <div> {results.map(result=>{result})} </div> </div> ); } } 複製代碼

使用 RxJS

// ...
import { BehaviorSubject } from 'rxjs';
import { debounce } from 'rxjs/operators';
class autocomp extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []
        }
        this.inputStream = new BehaviorSubject()
    }
    componentDidMount() {
        this.inputStream
            .pipe(
                debounce(100)
            )
            .subscribe(v => {
                const filteredRes = data.filter((item)=> {
                    // algorithm to search through the `data` array
                })
                this.setState({ results: filteredRes })
        })
    }
    render() {
        let { results } = this.state;
        return (
            <div className='autocomp_wrapper'> <input placeholder="Enter your search.." onChange={e => this.inputStream.next(e.target.value)} /> <div> {results.map(result => { result })} </div> </div> ); } } 複製代碼

重要領域:遊戲

有不少狀況須要使用節流和防抖,其中最須要的領域是遊戲。遊戲中最經常使用的動做是在電腦鍵盤或者遊戲手柄中按鍵,玩家可能常常按同一個鍵屢次(每 20 秒 40 次,即每秒 2 次)例如射擊、加速這樣的動做,但不管玩家按下射擊鍵的次數有多少,它只會發射一次(好比說每秒)。 因此使用節流控制爲 1 秒,這樣第二次按下按鈕將被忽略。

結論

咱們看到了節流和防抖如何提升 React 應用程序的性能,以及重複調用會影響性能,由於組件及其子樹將沒必要要地從新渲染,因此應該避免在 React 應用中重複調用方法。在小型程序中不會引發注意,但在大型程序中效果會很明顯。

❤️ 看完三件事

若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-
  2. 關注個人 GitHub,讓咱們成爲長期關係
  3. 關注公衆號「高級前端進階」,每週重點攻克一個前端面試重難點,公衆號後臺回覆「資料」 送你精選前端優質資料。

相關文章
相關標籤/搜索