Perform debounce in React.js
你如何在React.js中进行去抖动?
我想辩论handleOnChange。
我尝试使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function debounce(fn, delay) { var timer = null; return function() { var context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function() { fn.apply(context, args); }, delay); }; } var SearchBox = React.createClass({ render: function() { return <input type="search" name="p" onChange={this.handleOnChange} />; }, handleOnChange: function(event) { // make ajax call } }); |
2019:尝试钩子+承诺去抖
这是我如何解决这个问题的最新版本。我会用:
这是一些初始接线,但您自己组成原始块,并且您可以创建自己的自定义挂钩,这样您只需要执行一次。
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 | const useSearchStarwarsHero = () => { // Handle the input text state const [inputText, setInputText] = useState(''); // Debounce the original search async function const debouncedSearchStarwarsHero = useConstant(() => AwesomeDebouncePromise(searchStarwarsHero, 300) ); const search = useAsync( async text => { if (text.length === 0) { return []; } else { return debouncedSearchStarwarsHero(text); } }, // Ensure a new request is made everytime the text changes (even if it's debounced) [inputText] ); // Return everything needed for the hook consumer return { inputText, setInputText, search, }; }; |
然后你可以使用你的钩子:
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 | const SearchStarwarsHeroExample = () => { const { inputText, setInputText, search } = useSearchStarwarsHero(); return ( <input value={inputText} onChange={e => setInputText(e.target.value)} /> {search.loading && ...} {search.error && Error: {search.error.message}} {search.result && ( Results: {search.result.length} <p> {search.result.map(hero => ( <li key={hero.name}>{hero.name} </li> ))} </p>Ok. )} ); }; |
您将在此处找到此示例,您应该阅读react-async-hook文档以获取更多详细信息。
2018年:尝试承诺去债
我们经常要去除API调用以避免使用无用的请求充斥后端。
在2018年,使用回调(Lodash / Underscore)感觉不好并且容易出错。由于API调用以任意顺序解析,因此很容易遇到样板和并发问题。
我已经创建了一个带React的小库来解决你的痛苦:真棒 - 去抖 - 承诺。
这应该不是那么复杂:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 }); }; } |
去抖动功能可确保:
最后,如果您的组件卸载,您可以添加另一个技巧:
1 2 3 | componentWillUnmount() { this.setState = () => {}; } |
请注意,Observables(RxJS)也非常适合去抖动输入,但它是一种更强大的抽象,可能更难以正确学习/使用。
<2017:还是想使用回调去抖?
这里的重要部分是为每个组件实例创建一个去抖(或限制)函数。您不希望每次都重新创建去抖(或节流)功能,并且您不希望多个实例共享相同的去抖动功能。
我没有在这个答案中定义去抖函数,因为它不是真正相关的,但是这个答案对于
好主意:
因为去抖动函数是有状态的,所以我们必须为每个组件实例创建一个去抖动函数。
ES6(类属性):推荐
1 2 3 4 5 | class SearchBox extends React.Component { method = debounce(() => { ... }); } |
ES6(类构造函数)
1 2 3 4 5 6 7 | class SearchBox extends React.Component { constructor(props) { super(props); this.method = debounce(this.method.bind(this),1000); } method() { ... } } |
ES5
1 2 3 4 5 6 | var SearchBox = React.createClass({ method: function() {...}, componentWillMount: function() { this.method = debounce(this.method.bind(this),100); }, }); |
请参阅JsFiddle:3个实例每个实例生成1个日志条目(全局生成3个)。
不是一个好主意:
1 2 3 4 | var SearchBox = React.createClass({ method: function() {...}, debouncedMethod: debounce(this.method, 100); }); |
它不起作用,因为在类描述对象创建期间,
不是一个好主意:
1 2 3 4 5 6 7 | var SearchBox = React.createClass({ method: function() {...}, debouncedMethod: function() { var debounced = debounce(this.method,100); debounced(); }, }); |
这次你有效地创建了一个调用
不是一个好主意:
1 2 3 | var SearchBox = React.createClass({ debouncedMethod: debounce(function () {...},100), }); |
这在这里有点棘手。
该类的所有已安装实例将共享相同的去抖功能,并且通常这不是您想要的!请参阅JsFiddle:3个实例仅在全局范围内生成1个日志条目。
您必须为每个组件实例创建一个去抖动函数,而不是在每个组件实例共享的类级别的一个去抖动函数。
照顾React的事件池
这是相关的,因为我们经常想要去抖动或限制DOM事件。
在React中,您在回调中收到的事件对象(即
因此,如果您与原始回调异步访问
没有持久化(默认行为:池事件)
1 2 3 4 5 6 | onClick = e => { alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`); setTimeout(() => { alert(`async -> hasNativeEvent=${!!e.nativeEvent}`); }, 0); }; |
第二个(异步)将打印
坚持不懈
1 2 3 4 5 6 7 | onClick = e => { e.persist(); alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`); setTimeout(() => { alert(`async -> hasNativeEvent=${!!e.nativeEvent}`); }, 0); }; |
第二个(异步)将打印
你可以在这里测试这两个行为:JsFiddle
阅读Julen的答案,了解使用具有油门/去抖功能的
好。
不受控制的组件
您可以使用
下面使用下划线的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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
受控组件
更新:上面的示例显示了一个不受控制的组件。我一直使用受控元素,所以这是上面的另一个例子,但不使用
JSFiddle也可以使用。没有下划线的示例
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 42 43 44 45 46 47 48 49 50 51 52 53 | 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 ( <SearchBox query={this.state.result} handleSearch={this.handleSearch} /> <p> You searched for: {this.state.result} </p> ); } }); React.render(<Search query="Initial query" />, document.body); |
编辑:更新的示例和JSFiddles到React 0.12
编辑:更新示例以解决Sebastien Lorber提出的问题
编辑:使用jsfiddle更新,不使用下划线并使用普通的javascript去抖。
如果您需要从事件对象获取DOM输入元素,那么解决方案就更简单了 - 只需使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Item extends React.Component { constructor(props) { super(props); this.saveTitle = _.throttle(this.saveTitle.bind(this), 1000); } saveTitle(){ let val = this.inputTitle.value; // make the ajax call } render() { return <input ref={ el => this.inputTitle = el } type="text" defaultValue={this.props.title} onChange={this.saveTitle} /> } } |
我发现贾斯汀图尔克的这篇文章非常有帮助。经过几次尝试后,人们会认为反应/减少更正式的方式,它表明它由于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 29 30 31 32 33 34 35 | import React, {Component} from 'react' import TextField from 'material-ui/TextField' import { debounce } from 'lodash' class TableSearch extends Component { constructor(props){ super(props) this.state = { value: props.value } this.changeSearch = debounce(this.props.changeSearch, 250) } handleChange = (e) => { const val = e.target.value this.setState({ value: val }, () => { this.changeSearch(val) }) } render() { return ( <TextField className = {styles.field} onChange = {this.handleChange} value = {this.props.value} /> ) } } |
在对文本输入进行了一段时间的努力而没有找到完美的解决方案后,我在npm:react-debounce-input上找到了这个。
这是一个简单的例子:
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 ReactDOM from 'react-dom'; import {DebounceInput} from 'react-debounce-input'; class App extends React.Component { state = { value: '' }; render() { return ( <DebounceInput minLength={2} debounceTimeout={300} onChange={event => this.setState({value: event.target.value})} /> <p> Value: {this.state.value} </p> ); } } const appRoot = document.createElement('div'); document.body.appendChild(appRoot); ReactDOM.render(<App />, appRoot); |
DebounceInput组件接受您可以分配给普通输入元素的所有道具。在codepen上尝试一下
我希望它也可以帮助别人并节省一些时间。
如果您使用redux,您可以使用中间件以非常优雅的方式执行此操作。您可以将
1 2 3 4 5 6 7 8 9 10 11 12 | var timeout; export default store => next => action => { const { meta = {} } = action; if(meta.debounce){ clearTimeout(timeout); timeout = setTimeout(() => { next(action) }, meta.debounce) }else{ next(action) } } |
然后,您可以向操作创建者添加去抖动,例如:
1 2 3 4 5 | export default debouncedAction = (payload) => ({ type : 'DEBOUNCED_ACTION', payload : payload, meta : {debounce : 300} } |
实际上已经有中间件你可以下午npm为你做这件事。
这里有很多好消息,但要简洁明了。这对我有用......
1 2 3 4 5 6 7 8 | import React, {Component} from 'react'; import _ from 'lodash'; class MyComponent extends Component{ constructor(props){ super(props); this.handleChange = _.debounce(this.handleChange.bind(this),700); }; |
使用ES6 CLASS和React 15.x.x和lodash.debounce
我在这里使用React的refs,因为事件在内部损失了这个绑定。
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 | class UserInput extends React.Component { constructor(props) { super(props); this.state = { userInput:"" }; this.updateInput = _.debounce(this.updateInput, 500); } updateInput(userInput) { this.setState({ userInput }); //OrderActions.updateValue(userInput);//do some server stuff } render() { return ( <p> User typed: { this.state.userInput } </p> <input ref ="userValue" onChange = {() => this.updateInput(this.refs.userValue.value) } type ="text" / > ); } } ReactDOM.render( < UserInput / > , document.getElementById('root') ); |
1 2 3 | <script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"> |
您可以使用Lodash debounce https://lodash.com/docs/4.17.5#debounce方法。它简单而有效。
1 2 3 4 5 6 7 8 9 10 11 12 | import * as lodash from lodash; const update = (input) => { // Update the input here. console.log(`Input ${input}`); } const debounceHandleUpdate = lodash.debounce((input) => update(input), 200, {maxWait: 200}); doHandleChange() { debounceHandleUpdate(input); } |
您也可以使用以下方法取消去抖动方法。
1 | this.debounceHandleUpdate.cancel(); |
希望它能帮到你。干杯!!
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import React, { Component } from 'react'; import debounce from 'lodash/debounce' class ItemType extends Component { evntHandler = debounce((e) => { console.log(e) }, 500); render() { return ( <div className="form-field-wrap" onClick={e => { e.persist() this.evntHandler(e) }}> ... ); } } export default ItemType; |
有了功能组件,你可以这样做 -
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 | const Search = ({ getBooks, query }) => { const handleOnSubmit = (e) => { e.preventDefault(); } const debouncedGetBooks = debounce(query => { getBooks(query); }, 700); const onInputChange = e => { debouncedGetBooks(e.target.value) } return ( <Form className="search-books--form" onSubmit={handleOnSubmit}> <Form.Group controlId="formBasicEmail"> <Form.Control type="text" onChange={onInputChange} placeholder="Harry Potter" /> <Form.Text className="text-muted"> Search the world's most comprehensive index of full-text books. </Form.Text> </Form.Group> <Button variant="primary" type="submit"> Search </Button> </Form> ) } |
参考 -
- https://gist.github.com/elijahmanor/08fc6c8468c994c844213e4a4344a709
- https://blog.revathskumar.com/2016/02/reactjs-using-debounce-in-react-components.html
FYI
这是另一个PoC实现:
- 没有任何库(例如lodash)进行去抖动
- 使用React Hooks API
我希望它有帮助:)
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 | import React, { useState, useEffect, ChangeEvent } from 'react'; export default function DebouncedSearchBox({ inputType, handleSearch, placeholder, debounceInterval, }: { inputType?: string; handleSearch: (q: string) => void; placeholder: string; debounceInterval: number; }) { const [query, setQuery] = useState<string>(''); const [timer, setTimer] = useState<NodeJS.Timer | undefined>(); useEffect(() => { if (timer) { clearTimeout(timer); } setTimer(setTimeout(() => { handleSearch(query); }, debounceInterval)); }, [query]); const handleOnChange = (e: ChangeEvent<HTMLInputElement>): void => { setQuery(e.target.value); }; return ( <input type={inputType || 'text'} className="form-control" placeholder={placeholder} value={query} onChange={handleOnChange} /> ); } |
只是另一个变种与最近的反应和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 30 31 32 33 34 35 36 37 38 39 40 41 | class Filter extends Component { static propTypes = { text: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired } state = { initialText: '', text: '' } constructor (props) { super(props) this.setText = this.setText.bind(this) this.onChange = _.fp.debounce(500)(this.onChange.bind(this)) } static getDerivedStateFromProps (nextProps, prevState) { const { text } = nextProps if (text !== prevState.initialText) { return { initialText: text, text } } return null } setText (text) { this.setState({ text }) this.onChange(text) } onChange (text) { this.props.onChange(text) } render () { return (<input value={this.state.text} onChange={(event) => this.setText(event.target.value)} />) } } |
这是我想出的一个例子,用辩护者包装另一个类。这非常适合制作装饰器/高阶函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | export class DebouncedThingy extends React.Component { static ToDebounce = ['someProp', 'someProp2']; constructor(props) { super(props); this.state = {}; } // On prop maybe changed componentWillReceiveProps = (nextProps) => { this.debouncedSetState(); }; // Before initial render componentWillMount = () => { // Set state then debounce it from here on out (consider using _.throttle) this.debouncedSetState(); this.debouncedSetState = _.debounce(this.debouncedSetState, 300); }; debouncedSetState = () => { this.setState(_.pick(this.props, DebouncedThingy.ToDebounce)); }; render() { const restOfProps = _.omit(this.props, DebouncedThingy.ToDebounce); return <Thingy {...restOfProps} {...this.state} /> } } |
而不是将handleOnChange包装在debounce()中,为什么不将ajax调用包装在debounce内部的回调函数内,从而不会破坏事件对象。所以像这样:
1 2 3 4 5 | handleOnChange: function (event) { debounce( $.ajax({}) , 250); } |
我正在寻找同一问题的解决方案并且遇到了这个线程以及其他一些但是他们遇到了同样的问题:如果你想要做一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // at top of file: var myAction = require('../actions/someAction'); // inside React.createClass({...}); handleOnChange: function (event) { var value = event.target.value; var doAction = _.curry(this.context.executeAction, 2); // only one parameter gets passed into the curried function, // so the function passed as the first parameter to _.curry() // will not be executed until the second parameter is passed // which happens in the next function that is wrapped in _.debounce() debouncedOnChange(doAction(myAction), value); }, debouncedOnChange: _.debounce(function(action, value) { action(value); }, 300) |
一个漂亮而干净的解决方案,不需要任何外部依赖:
与React Hooks辩论
它使用自定义加上useEffect React钩子和
对于
1 2 3 4 5 6 | updateUserProfileField(fieldName) { const handler = throttle(value => { console.log(fieldName, value); }, 400); return evt => handler(evt.target.value.trim()); } |
并在您的
1 | <input onChange={this.updateUserProfileField("givenName").bind(this)}/> |
每次调用它时,
注意不要尝试直接返回处理程序,例如这不起作用:
1 2 3 4 5 | updateUserProfileField(fieldName) { return evt => throttle(value => { console.log(fieldName, value); }, 400)(evt.target.value.trim()); } |
之所以不行,是因为每次调用事件时都会生成一个新的油门功能,而不是使用相同的油门功能,所以油门基本没用;)
此外,如果您使用
避免使用
类
1 2 3 4 5 6 7 | handleMouseOver = throttle(target => { console.log(target); }, 1000); onMouseOver = e => { this.handleMouseOver(e.target); }; |
职能
1 2 3 4 5 6 7 | const handleMouseOver = useRef(throttle(target => { console.log(target); }, 1000)); function onMouseOver(e) { handleMouseOver.current(e.target); } |
请注意,如果
这是一个有效的TypeScript示例,适用于那些使用TS并想要去抖动
1 2 3 4 5 6 7 8 9 10 | function debounce<T extends (...args: any[]) => any>(time: number, func: T): (...funcArgs: Parameters< T >) => Promise<ReturnType< T >> { let timeout: Timeout; return (...args: Parameters< T >): Promise<ReturnType< T >> => new Promise((resolve) => { clearTimeout(timeout); timeout = setTimeout(() => { resolve(func(...args)); }, time) }); } |
你可以使用tlence tlence
1 2 3 4 5 6 7 8 9 10 11 12 | function log(server) { console.log('connecting to', server); } const debounceLog = debounce(log, 5000); // just run last call to 5s debounceLog('local'); debounceLog('local'); debounceLog('local'); debounceLog('local'); debounceLog('local'); debounceLog('local'); |
这里有点晚了,但这应该有所帮助。
创建这个类(它用typescript编写,但很容易将其转换为javascript)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | export class debouncedMethod< T >{ constructor(method:T, debounceTime:number){ this._method = method; this._debounceTime = debounceTime; } private _method:T; private _timeout:number; private _debounceTime:number; public invoke:T = ((...args:any[])=>{ this._timeout && window.clearTimeout(this._timeout); this._timeout = window.setTimeout(()=>{ (this._method as any)(...args); },this._debounceTime); }) as any; } |
并使用
1 2 3 4 | var foo = new debouncedMethod((name,age)=>{ console.log(name,age); },500); foo.invoke("john",31); |
Julen解决方案有点难以理解,对于任何根据标题而不是问题的细节而绊倒他的人来说,这里的代码更清晰,更具针对性。
tl; dr版本:当您更新到观察者时,发送调用调度方法而不是反过来实际上会通知观察者(或执行ajax等)
使用示例组件jsfiddle完成jsfiddle
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 42 43 44 | var InputField = React.createClass({ getDefaultProps: function () { return { initialValue: '', onChange: null }; }, getInitialState: function () { return { value: this.props.initialValue }; }, render: function () { var state = this.state; return ( <input type="text" value={state.value} onChange={this.onVolatileChange} /> ); }, onVolatileChange: function (event) { this.setState({ value: event.target.value }); this.scheduleChange(); }, scheduleChange: _.debounce(function () { this.onChange(); }, 250), onChange: function () { var props = this.props; if (props.onChange != null) { props.onChange.call(this, this.state.value) } }, }); |
你也可以使用自编的mixin,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 | var DebounceMixin = { debounce: function(func, time, immediate) { var timeout = this.debouncedTimeout; if (!timeout) { if (immediate) func(); this.debouncedTimeout = setTimeout(function() { if (!immediate) func(); this.debouncedTimeout = void 0; }.bind(this), time); } } }; |
然后在你的组件中使用它,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var MyComponent = React.createClass({ mixins: [DebounceMixin], handleClick: function(e) { this.debounce(function() { this.setState({ buttonClicked: true }); }.bind(this), 500, true); }, render: function() { return ( <button onClick={this.handleClick}></button> ); } }); |