乐闻世界logo
搜索文章和话题

React 中如何实现 debounce 防抖函数?

7 个月前提问
3 个月前修改
浏览次数42

6个答案

1
2
3
4
5
6

在React中实现防抖函数(debounce function)通常涉及到在组件中使用一个特定的函数,该函数能够延迟执行实际的处理逻辑,直到停止触发一段时间后。这样做可以减少像输入框连续输入这样的事件处理函数的调用次数,从而提高性能。

下面是在React组件中实现防抖功能的步骤:

  1. 创建防抖函数: 创建一个防抖函数,该函数会接收一个要延迟执行的函数 func 和延迟执行的时间 wait
javascript
const debounce = (func, wait) => { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; };
  1. 在React组件中使用防抖函数: 使用 useEffect 钩子配合防抖函数实现防抖效果。

假设我们有一个搜索输入框,我们想要在用户停止输入一段时间后再触发搜索,避免每次键入都进行搜索:

javascript
import React, { useState, useEffect } from 'react'; const SearchComponent = () => { const [inputValue, setInputValue] = useState(''); // 定义防抖函数 const debounceSearch = debounce((searchValue) => { console.log(`搜索: ${searchValue}`); // 在这里执行搜索逻辑,例如 API 调用 }, 500); useEffect(() => { if (inputValue) { debounceSearch(inputValue); } }, [inputValue]); return ( <input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} placeholder="输入关键词进行搜索" /> ); }; export default SearchComponent;

上面的例子中,我们在输入框的 onChange 事件中设置了 inputValue 的值,然后我们在 useEffect 中使用 inputValue 作为依赖项,这表示当 inputValue 改变时,useEffect 的回调会被执行。在 useEffect 的回调中,我们调用了 debounceSearch 函数,它会延迟执行搜索逻辑,直到用户停止输入500毫秒后。

这样,无论用户输入多快,真正的搜索逻辑只会在用户停止输入一段时间后才会执行,大大减少了不必要的处理和API调用。

2024年6月29日 12:07 回复

2019:尝试 hooks + Promise debouncing

这是我解决此问题的最新版本。我会用:

这是一些初始接线,但是您正在自己编写原始块,并且您可以创建自己的自定义挂钩,这样您只需要做一次。

shell
// Generic reusable hook const useDebouncedSearch = (searchFunction) => { // Handle the input text state const [inputText, setInputText] = useState(''); // Debounce the original search async function const debouncedSearchFunction = useConstant(() => AwesomeDebouncePromise(searchFunction, 300) ); // The async callback is run each time the text changes, // but as the search function is debounced, it does not // fire a new request on each keystroke const searchResults = useAsync( async () => { if (inputText.length === 0) { return []; } else { return debouncedSearchFunction(inputText); } }, [debouncedSearchFunction, inputText] ); // Return everything needed for the hook consumer return { inputText, setInputText, searchResults, }; };

然后你可以使用你的钩子:

shell
const useSearchStarwarsHero = () => useDebouncedSearch(text => searchStarwarsHeroAsync(text)) const SearchStarwarsHeroExample = () => { const { inputText, setInputText, searchResults } = useSearchStarwarsHero(); return ( <div> <input value={inputText} onChange={e => setInputText(e.target.value)} /> <div> {searchResults.loading && <div>...</div>} {searchResults.error && <div>Error: {search.error.message}</div>} {searchResults.result && ( <div> <div>Results: {search.result.length}</div> <ul> {searchResults.result.map(hero => ( <li key={hero.name}>{hero.name}</li> ))} </ul> </div> )} </div> </div> ); };

您会发现此示例在此处运行,您应该阅读react-async-hook文档以获取更多详细信息。


2018:尝试消除承诺

我们经常希望消除 API 调用的反跳,以避免无用的请求淹没后端。

在 2018 年,使用回调(Lodash / Underscore.js)对我来说感觉很糟糕并且容易出错。由于 API 调用以任意顺序解析,因此很容易遇到样板文件和并发问题。

我创建了一个考虑到 React 的小库来解决你的痛苦:awesome-debounce-promise

这不应该比以下更复杂:

shell
const searchAPI = text => fetch('/search?text=' + encodeURIComponent(text)); const searchAPIDebounced = AwesomeDebouncePromise(searchAPI, 500); class SearchInputAndResults extends React.Component { state = { text: '', results: null, }; handleTextChange = async text => { this.setState({ text, results: null }); const result = await searchAPIDebounced(text); this.setState({ result }); }; }

去抖功能可确保:

  • API 调用将被反跳
  • 去抖函数总是返回一个承诺
  • 只有最后一个调用返回的承诺才会解决
  • this.setState({ result });每次API 调用都会发生一次

最后,如果您的组件卸载,您可以添加另一个技巧:

shell
componentWillUnmount() { this.setState = () => {}; }

请注意,ObservablesRxJS)也非常适合消除输入的抖动,但它是一个更强大的抽象,可能更难正确学习/使用。


< 2017:仍然想使用回调去抖动?

这里重要的部分是为每个组件实例创建一个去抖动(或节流)函数。您不想每次都重新创建去抖(或节流)函数,并且不希望多个实例共享相同的去抖函数。

我没有在这个答案中定义去抖动函数,因为它并不真正相关,但是这个答案可以与_.debounceUnderscore.js 或 Lodash 以及任何用户提供的去抖动函数完美配合。


好主意:

由于去抖函数是有状态的,因此我们必须为每个组件实例创建一个去抖函数

ES6(类属性):推荐

shell
class SearchBox extends React.Component { method = debounce(() => { ... }); }

ES6(类构造函数)

shell
class SearchBox extends React.Component { constructor(props) { super(props); this.method = debounce(this.method.bind(this),1000); } method() { ... } }

ES5

shell
var SearchBox = React.createClass({ method: function() {...}, componentWillMount: function() { this.method = debounce(this.method.bind(this),100); }, });

请参阅JSFiddle:三个实例每个实例生成一个日志条目(全局为三个)。


_这不是_一个好主意:
shell
var SearchBox = React.createClass({ method: function() {...}, debouncedMethod: debounce(this.method, 100); });

这是行不通的,因为在类描述对象创建过程中,this对象本身并不是创建的。this.method不会返回您期望的内容,因为this上下文不是对象本身(顺便说一句,它实际上还不存在,因为它刚刚被创建)。


_这不是_一个好主意:
shell
var SearchBox = React.createClass({ method: function() {...}, debouncedMethod: function() { var debounced = debounce(this.method,100); debounced(); }, });

这次,您实际上创建了一个去抖函数,该函数调用您的this.method. 问题是您在每次debouncedMethod调用时都重新创建它,因此新创建的去抖函数对以前的调用一无所知!随着时间的推移,您必须重复使用相同的去抖函数,否则去抖将不会发生。


_这不是_一个好主意:
shell
var SearchBox = React.createClass({ debouncedMethod: debounce(function () {...},100), });

这里有点棘手。

该类的所有已安装实例将共享相同的去抖函数,而大多数情况下这不是您想要的!请参阅JSFiddle:三个实例在全局范围内仅生成一个日志条目。

您必须为每个组件实例创建一个去抖函数,而不是在类级别创建由每个组件实例共享的单个去抖函数。


照顾 React 的事件池

这是相关的,因为我们经常想要消除或限制 DOM 事件。

在 React 中,您在回调中收到的事件对象(即SyntheticEvent)被池化(现在已记录)。这意味着在调用事件回调后,您收到的 SyntheticEvent 将以空属性放回池中,以减少GC压力。

因此,如果您SyntheticEvent与原始回调异步访问属性(如果您限制/反跳,可能会出现这种情况),您访问的属性可能会被删除。如果您希望事件永远不会被放回池中,您可以使用该persist()方法。

没有持久化(默认行为:池化事件)
shell
onClick = e => { alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`); setTimeout(() => { alert(`async -> hasNativeEvent=${!!e.nativeEvent}`); }, 0); };

第二个(异步)将打印hasNativeEvent=false,因为事件属性已被清理。

凭着坚持
shell
onClick = e => { e.persist(); alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`); setTimeout(() => { alert(`async -> hasNativeEvent=${!!e.nativeEvent}`); }, 0); };

第二个(异步)将打印hasNativeEvent=true,因为persist允许您避免将事件放回池中。

您可以在这里测试这两种行为:JSFiddle

阅读Julen 的答案persist(),了解使用油门/去抖功能的示例。

2024年6月29日 12:07 回复

不受控制的组件

您可以使用该event.persist()方法

下面是使用Underscore.js '的示例_.debounce()

shell
var SearchBox = React.createClass({ componentWillMount: function () { this.delayedCallback = _.debounce(function (event) { // `event.target` is accessible now }, 1000); }, onChange: function (event) { event.persist(); this.delayedCallback(event); }, render: function () { return ( <input type="search" onChange={this.onChange} /> ); } });

请参阅这个 JSFiddle


受控组件

上面的例子显示了一个不受控制的组件。我一直使用受控元素,所以这是上面的另一个例子,但没有使用event.persist()“欺骗”。

JSFiddle也可用。不带下划线的示例

shell
var SearchBox = React.createClass({ getInitialState: function () { return { query: this.props.query }; }, componentWillMount: function () { this.handleSearchDebounced = _.debounce(function () { this.props.handleSearch.apply(this, [this.state.query]); }, 500); }, onChange: function (event) { this.setState({query: event.target.value}); this.handleSearchDebounced(); }, render: function () { return ( <input type="search" value={this.state.query} onChange={this.onChange} /> ); } }); var Search = React.createClass({ getInitialState: function () { return { result: this.props.query }; }, handleSearch: function (query) { this.setState({result: query}); }, render: function () { return ( <div id="search"> <SearchBox query={this.state.result} handleSearch={this.handleSearch} /> <p>You searched for: <strong>{this.state.result}</strong></p> </div> ); } }); React.render(<Search query="Initial query" />, document.body);
2024年6月29日 12:07 回复

2019:使用“useCallback”反应钩子

在尝试了许多不同的方法之后,我发现 usinguseCallback是解决事件debounce中使用的多次调用问题的最简单且最有效的方法onChange

根据Hooks API 文档

useCallback 返回回调的记忆版本,仅当依赖项之一发生更改时该版本才会更改。

将空数组作为依赖项传递可确保回调仅被调用一次。这是一个简单的实现:

shell
import React, { useCallback } from "react"; import { debounce } from "lodash"; const handler = useCallback(debounce(someFunction, 2000), []); const onChange = (event) => { // perform any event related action here handler(); };
2024年6月29日 12:07 回复

在与文本输入斗争了一段时间并且自己没有找到完美的解决方案之后,我在 npm 上找到了这个:react-debounce-input

这是一个简单的例子:

shell
import React from 'react'; import ReactDOM from 'react-dom'; import {DebounceInput} from 'react-debounce-input'; class App extends React.Component { state = { value: '' }; render() { return ( <div> <DebounceInput minLength={2} debounceTimeout={300} onChange={event => this.setState({value: event.target.value})} /> <p>Value: {this.state.value}</p> </div> ); } } const appRoot = document.createElement('div'); document.body.appendChild(appRoot); ReactDOM.render(<App />, appRoot);

DebounceInput 组件接受您可以分配给普通输入元素的所有属性。在CodePen上尝试一下。

2024年6月29日 12:07 回复

可以有一种使用React hooks 的简单方法。

第 1 步:定义一个状态来维护搜索到的文本

shell
const [searchTerm, setSearchTerm] = useState('')

步骤 2:使用 useEffect 捕获任何变化searchTerm

shell
useEffect(() => { const delayDebounceFn = setTimeout(() => { if (searchTerm) { // Write your logic here } }, 400) return () => clearTimeout(delayDebounceFn) }, [searchTerm])

第三步:编写一个函数来处理输入变化

shell
function handleInputChange(value) { if (value) { setSearchTerm(value) } }

就这样!在需要时调用此方法。

2024年6月29日 12:07 回复

你的答案