在 React.js 中执行去抖动

IT技术 javascript reactjs
2021-01-18 14:23:54

你如何在 React.js 中执行去抖动?

我想去抖handleOnChange。

我试过,debounce(this.handleOnChange, 200)但它不起作用。

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
  }
});
6个回答

2019 年:尝试钩子 + Promise去抖动

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

这是一些初始接线,但您正在自己组合原始块,并且您可以制作自己的自定义钩子,这样您只需要做一次。

// 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,
  };
};

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

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 年:尝试Promise去抖动

我们经常希望对 API 调用进行 debounce,以避免用无用的请求淹没后端。

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

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

这不应该比这更复杂:

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 调用将被去抖动
  • 去抖动函数总是返回一个promise
  • 只有最后一次调用返回的Promise会解决
  • this.setState({ result });每个 API 调用都会发生一个

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

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

请注意,Observables (RxJS) 也非常适合输入去抖动,但它是一种更强大的抽象,可能更难正确学习/使用。


< 2017:还想用回调去抖?

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

我没有在这个答案中定义去抖动函数,因为它并不是真正相关的,但是这个答案_.debounce对于下划线或 lodash 以及任何用户提供的去抖动函数都可以很好地工作


好主意:

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

ES6(类属性):推荐

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

ES6(类构造函数)

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

ES5

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

请参阅JsFiddle:3 个实例为每个实例生成 1 个日志条目(全局生成 3 个)。


不是一个好主意:

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

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


不是一个好主意:

var SearchBox = React.createClass({
  method: function() {...},
  debouncedMethod: function() {
      var debounced = debounce(this.method,100);
      debounced();
  },
});

这一次,您将有效地创建一个去抖动函数,该函数调用您的this.method. 问题是你在每次debouncedMethod调用时都重新创建它,所以新创建的 debounce 函数对以前的调用一无所知!随着时间的推移,您必须重用相同的去抖动函数,否则去抖动将不会发生。


不是一个好主意:

var SearchBox = React.createClass({
  debouncedMethod: debounce(function () {...},100),
});

这在这里有点棘手。

该类的所有挂载实例都将共享相同的去抖动函数,而且大多数情况下这不是您想要的!请参阅JsFiddle:3 个实例在全球范围内仅生成 1 个日志条目。

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


照顾 React 的事件池

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

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

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

没有持久化(默认行为:池化事件)

onClick = e => {
  alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`);
  setTimeout(() => {
    alert(`async -> hasNativeEvent=${!!e.nativeEvent}`);
  }, 0);
};

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

随着坚持

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

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

您可以在此处测试这 2 种行为:JsFiddle

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

请注意,在 ES6 中,您可以handleOnChange = debounce((e) => { /* onChange handler code here */ }, timeout)在类的顶层执行,而不是在构造函数中定义您的方法(感觉很奇怪)您仍然有效地设置了一个实例成员,但它看起来更像是一个普通的方法定义。constructor如果您还没有定义一个,则不需要。我想这主要是一种风格偏好。
2021-03-12 14:23:54
@JonasKello 你不能在无状态组件内部去抖动,因为去抖动的函数实际上是有状态的。您需要一个有状态的组件来保存去抖动的函数,但如果需要,您可以使用已经去抖动的函数调用无状态组件。
2021-03-13 14:23:54
不要忘了取消去抖方法componentWillUnmountthis.method.cancel()-否则它可能要对setState已卸载的组件。
2021-03-16 14:23:54
出色的答案,这非常适合将表单字段状态设置为“正在交互”几秒钟后停止输入,然后可以取消表单提交或 onBlur
2021-03-18 14:23:54
为什么所有答案都包含 _.debounce 而不是编写函数?它需要该功能的整个库?
2021-04-07 14:23:54

不受控制的组件

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

下面是一个使用下划线的示例_.debounce()

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没有下划线的例子

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);

编辑:更新示例和 JSFiddles 到 React 0.12

编辑:更新示例以解决 Sebastien Lorber 提出的问题

编辑:使用不使用下划线并使用纯 javascript 去抖动的 jsfiddle 更新。

这不适用于输入。去抖动函数中的事件目标不再具有值...因此输入保持为空。
2021-03-10 14:23:54
@AlastairMaw 这个问题有一个不受控制的成分,这就是为什么回复也有。我在下面添加了受控组件的替代版本,并带有预先填充的值。
2021-03-14 14:23:54
如果在 DOM 中多次挂载组件,这是非常危险的,请参阅stackoverflow.com/questions/23123138/...
2021-03-17 14:23:54
有点复杂,这个。你必须要小心道具。如果您设置,<input value={this.props.someprop}...那么它将无法正确呈现,因为按键上的更新直到去抖动后才会返回到组件中。value=如果您对此不受管理感到高兴,则可以省略它,但是如果您想预先填充该值和/或将其绑定到其他地方,那么显然这不起作用。
2021-04-03 14:23:54
虽然这是一个很好的答案,但我不建议使用,persist特别是当可能有很多事件时,例如 on mousemove我已经看到代码变得完全没有响应。在事件调用中从本机事件中提取所需数据,然后仅使用数据而不是事件本身调用去抖动/节流函数,效率要高得多。无需以这种方式持久化事件
2021-04-03 14:23:54

2019 年:使用“useCallback”react-hooks

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

根据Hooks API 文档

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

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

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();
 };

希望这可以帮助!

这是我如何使用此解决方案让用户输入输入,然后在他完成输入后发送带有输入值的去抖动 API 调用。stackoverflow.com/questions/59358092/…
2021-03-12 14:23:54
您能否首先解释为什么会发生多次通话?难道debounce()不考虑onChange()回调是相同的回调方法?
2021-03-13 14:23:54
添加到上面的答案---- const someFunction = (text) => { dispatch({ type: "addText", payload: { id, text, }, }); }; <input type="text" defaultValue={text} onChange={(e) => handler(e.target.value)} />
2021-03-27 14:23:54
如果您使用钩子,这是一个很好的解决方案。你为我节省了更多的挫败感。谢谢!
2021-04-07 14:23:54
我修改了这个解决方案,让它在我的应用程序中工作。首先,我必须const testFunc2 = useCallback(debounce((text) => console.log('testFunc2() has ran:', text), 1000) , []);在函数组件的主体内移动该行,否则 React 会在其外部输出有关钩子使用的错误消息。然后在onChange事件处理程序:<input type='text' name='name' className='th-input-container__input' onChange={evt => {testFunc2(evt.target.value);}}
2021-04-08 14:23:54

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

这是一个简单的例子:

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 组件接受您可以分配给普通输入元素的所有props。codepen试试

我希望它也能帮助其他人并为他们节省一些时间。

在尝试了这里列出的许多解决方案之后,绝对是最简单的。
2021-03-24 14:23:54
这确实是更好的解决方案!不仅因为它使用最少的代码,它还允许对类函数进行 debounce(与 awesome-debounce-promise 不同,因为这个原因几乎没用)
2021-03-24 14:23:54

我发现Justin Tulk 的这篇文章很有帮助。经过几次尝试,在人们认为是更正式的 react/redux 方式之后,它表明由于React 的合成事件池而失败然后,他的解决方案使用一些内部状态来跟踪输入中更改/输入的值,然后立即进行回调setState,调用节流/去抖动 redux 操作,实时显示一些结果。

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}
        />
    )
  }
}
状态组件的好解决方案。
2021-03-13 14:23:54