码云笔记前端博客
Home > 前端技术 > Throttle和Debounce在React中的应用

Throttle和Debounce在React中的应用

2019-06-20 分类:前端技术 作者:码云 阅读(2414)

本文共计7549个字,预计阅读时长需要19分钟。

使用React构建应用程序时,我们总是会遇到一些限制问题,比如大量的调用,异步网络请求和DOM更新等,我们可以使用React提供的功能来检查这些。

  • shouldComponentUpdate(…) 生命周期钩子
  • React.PureComponent
  • React.memo
  • 窗口化和虚拟化
  • 记忆化
  • 水化
  • 挂钩(useState,useMemo,useContext,useReducer,等)

在这篇文章中,我们将研究如何在不使用阵营提供的功能下来改进阵营应用程序性能,我们将使用一种不仅仅适用于阵营的技术:节流(油门)和防抖(去抖动)。

实例开始

实例1

下面这个例子可以很好的解释节流和防抖带给我们的好处,我们假设有一个autocomp组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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一起出现。

1
2
3
4
5
6
7
8
9
10
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状语从句:其他查询查询结果一起出现。

即使autocomp可以在没有网络请求的情况下完成(例如,内存中有一个本地“数据库”),仍然需要为输入的每个字符/单词进行昂贵的DOM更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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

另一个例子是使用resize和scroll等事件。大多数情况下,网站每秒滚动1000次,想象一下在scroll事件中添加一个事件处理。

1
2
3
document.body.addEventListener('scroll', ()=> {
    console.log('Scrolled !!!')
})

你会发现这个函数每秒被执行1000次!如果这个事件处理函数执行大量计算或大量DOM操作,将面临最坏的情况。

1
2
3
4
5
6
7
8
9
10
11
12
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更新,我们需要等到用户停止输入或者输入一段时间之后,等到用户停止滚动或者滚动一段时间之后,再去执行事件处理函数。

所有这些确保我们的应用程序有良好性能,让我们看看如何使用节流和防抖来避免这种性能瓶颈。

节流节流

节流强制一个函数在一段时间内可以调用的最大次数,例如每100毫秒最多执行一次函数。

节流是指在指定的时间内执行一次给定的函数。这限制了函数被调用的次数,所以重复的函数调用不会重置任何数据。

假设我们通常以1000次/ 20秒的速度调用函数。如果我们使用节流将它限制为每500毫秒执行一次,我们会看到函数在20秒内将执行40次。

1
2
1000 * 20 secs = 20,000ms
20,000ms / 500ms = 40 times

这是从1000次到40次的极大优化。

下面将介绍在阵营中使用节流的例子,将分别使用underscore,lodash,RxJS以及自定义实现。

使用下划线

将我们使用underscore提供的节流函数处理我们的autocomp组件。

先安装依赖。

1
npm i underscore

然后在组件中导入它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ...
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程序中使用它。

首先,我们需要安装依赖。

1
npm i lodash

使用lodash,的我们autocomp将的英文这样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ...
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。

1
npm i rxjs

从我们rxjs库导入throttle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// ...
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中导入了throttle状语从句:BehaviorSubject,了初始化一个BehaviorSubject实例保存在inputStream属性,在componentDidMount中,将我们inputStream流传递给节流操作符,传入1000,表示RxJS节流控制为1000毫秒,节流操作返回的流被订阅以获得流值。

因为在组件加载时订阅了inputStream,所以我们开始输入时,输入的内容就被发送到inputStream流中。刚开始时,由于throttle操作符1000ms内不会发送内容,在这之后发送最新值,发送之后就开始计算得到结果。

如果我们以200ms 1个字母的速度输入fidudusola,该组件将重新渲染2000ms/1000ms=2次。

使用自定义实现

我们实现自己的节流函数,方便更好的理解节流如何工作。

我们知道在一个节流控制的函数中,它会根据指定的时间间隔调用,我们将使用setTimeout函数实现这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function throttle(fn, ms) {
    let timeout
    function exec() {
        fn.apply()
    }
    function clear() {
        timeout == undefined ? null : clearTimeout(timeout)
    }
    if(fn !== undefined &amp;&amp; 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();
    }
}

上面的实现非常简单,直接使用setTimeout API实现,在React项目中使用方式如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ...
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>
        );
    }
}

防抖辩护

防抖会强制自上次调用后经过一定时间才会再次调用函数,例如只有在没有被调用的情况下经过一段时间之后(例如100毫秒)才执行该函数。

在防抖时,它忽略对函数的所有调用,直到函数停止调用一段时间之后才会再次执行。

下面将介绍在项目中使用debounce的例子。

使用下划线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ...
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ...
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// ...
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秒,这样第二次按下按钮将被忽略。

结语

我们看到了节流和防抖如何提高在线应用程序的性能中不会引起注意,但在大型程序中效果会很明显。

「除特别注明外,本站所有文章均为码云笔记原创,转载请保留出处!」

赞(0) 打赏

觉得文章有用就打赏一下文章作者

支付宝
微信
0

觉得文章有用就打赏一下文章作者

支付宝
微信

上一篇:

下一篇:

你可能感兴趣

共有 0 条评论 - Throttle和Debounce在React中的应用

博客简介

码云笔记网 mybj123.com,一个专注Web前端开发技术的博客,主要记录和总结博主在前端开发工作中常用的实战技能及前端资源分享,分享各种科普知识和实用优秀的代码,以及分享些热门的互联网资讯和福利!码云笔记网有你更精彩!
更多博客详情请看关于博客

精彩评论