如何在React.js中执行debounce ?

我想撤消handleOnChange。

我尝试debounce(这。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
  }
});

当前回答

2019年:尝试钩子+承诺跳跃

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

awesome- debount -promise来debount async函数 使用-constant将该函数存储到组件中 React-async-hook来获取结果到组件中

这是一些最初的连接,但你是在自己组成原始块,你可以让你自己的自定义钩子,这样你只需要这样做一次。

// 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年:尝试打破承诺

我们经常希望取消API调用,以避免后端充斥着无用的请求。

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

我用React创建了一个小库来解决你的烦恼:awesome-debounc -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 });
  };
}

debound函数确保:

API调用将被撤销 debented函数总是返回一个promise 只有最后一个电话的承诺才能解决问题 一个这个。setState({result});每个API调用会发生什么

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

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

注意,Observables (RxJS)也非常适合用于分解输入,但它是一个更强大的抽象,可能更难正确学习/使用。


< 2017:还想用回调解调吗?

这里的重要部分是为每个组件实例创建单个debpublished(或throttled)函数。您不希望每次都重新创建debounce(或throttle)函数,也不希望多个实例共享同一个debounce函数。

我没有在这个答案中定义一个deboundation函数,因为它并不真正相关,但这个答案将与_.debounce的下划线或lodash以及任何用户提供的deboundation函数完美地工作。


好主意:

因为debound函数是有状态的,所以我们必须为每个组件实例创建一个debound函数。

ES6 (class属性):推荐

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

它不会工作,因为在类描述对象创建期间,这不是创建的对象本身。这一点。方法不会返回你期望的内容,因为这个上下文不是对象本身(它实际上还不存在,因为它刚刚被创建)。


这不是个好主意:

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

这一次,您有效地创建了一个调用This .method的debaded函数。问题是您在每次debouncedMethod调用时都要重新创建它,因此新创建的debounce函数不知道以前的调用!您必须在一段时间内重用相同的回调函数,否则将不会发生回调。


这不是个好主意:

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

这有点棘手。

类的所有挂载实例都将共享相同的debked函数,这通常不是您想要的!请参阅JsFiddle: 3个实例全局只生成1个日志条目。

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


负责React的事件池

这是相关的,因为我们经常想要反弹或抑制DOM事件。

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

因此,如果您以异步方式访问SyntheticEvent属性到原始回调(如果您节流/debounce,可能会出现这种情况),您访问的属性可能会被擦除。如果希望事件永远不被放回池中,可以使用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,因为持久化允许您避免将事件放回池中。

您可以在这里测试这两个行为:JsFiddle

请阅读Julen的回答,了解在throttle/debounce函数中使用persist()的示例。

其他回答

避免使用event.persist()——你想让React回收合成事件。我认为无论你使用类还是钩子,最干净的方法是将回调函数分成两部分:

没有deboundation的回调 只使用您需要的事件片段调用已撤销的函数(这样合成的事件就可以循环使用)

handleMouseOver = throttle(target => {
  console.log(target);
}, 1000);

onMouseOver = e => {
  this.handleMouseOver(e.target);
};

<div onMouseOver={this.onMouseOver} />

功能

const handleMouseOver = useRef(throttle(target => {
  console.log(target);
}, 1000));

function onMouseOver(e) {
  handleMouseOver.current(e.target);
}

<div onMouseOver={this.onMouseOver} />

注意,如果你的handleMouseOver函数从组件中使用状态,你应该使用usemo而不是useRef,并将它们作为依赖项传递,否则你将使用过时的数据(当然不适用于类)。

我的解决方案是基于钩子(用Typescript编写)。

我有2个主要钩子useDebouncedValue和useDebouncedCallback

First - useDebouncedValue

假设我们有一个搜索框,但是我们想在用户停止输入0.5秒后向服务器请求搜索结果

function SearchInput() {
  const [realTimeValue, setRealTimeValue] = useState('');

  const debouncedValue = useDebouncedValue(realTimeValue, 500); // this value will pick real time value, but will change it's result only when it's seattled for 500ms

  useEffect(() => {
    // this effect will be called on seattled values
    api.fetchSearchResults(debouncedValue);
  }, [debouncedValue])

  return <input onChange={event => setRealTimeValue(event.target.value)} />
}

实现

import { useState, useEffect } from "react";

export function useDebouncedValue<T>(input: T, time = 500) {
  const [debouncedValue, setDebouncedValue] = useState(input);

  // every time input value has changed - set interval before it's actually commited
  useEffect(() => {
    const timeout = setTimeout(() => {
      setDebouncedValue(input);
    }, time);

    return () => {
      clearTimeout(timeout);
    };
  }, [input, time]);

  return debouncedValue;
}

第二个useDebouncedCallback

它只是在你的组件范围内创建一个“debpublished”函数。

假设我们有一个带有按钮的组件,在你停止点击它后,它将显示警报500毫秒。

function AlertButton() {
  function showAlert() {
    alert('Clicking has seattled');
  }

  const debouncedShowAlert = useDebouncedCallback(showAlert, 500);

  return <button onClick={debouncedShowAlert}>Click</button>
}

实现(注意我使用lodash/debounce作为辅助)

import debounce from 'lodash/debounce';
import { useMemo } from 'react';

export function useDebouncedCallback<T extends (...args: any) => any>(callback: T, wait?: number) {
  const debouncedCallback = useMemo(() => debounce(callback, wait), [callback, wait]);

  return debouncedCallback;
}

与其在debounce()中包装handleOnChange,不如在debounce()中包装回调函数中的ajax调用,从而不破坏事件对象。就像这样:

handleOnChange: function (event) {
   debounce(
     $.ajax({})
  , 250);
}

不受控制的组件

你可以使用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提出的问题

编辑:更新的jsfiddle不使用下划线和使用纯javascript debounce。

如果你只需要在一个按钮中执行一个请求数据的debounce,提供的代码可能对你有帮助:

创建一个函数,以防止在请求为真或假时使用默认的条件语句 实现useState钩子和useEffect钩子

const PageOne = () => {
 const [requesting, setRequesting] = useState(false);

  useEffect(() => {
    return () => {
      setRequesting(false);
    };
  }, [requesting]);

  const onDebounce = (e) => {
    if (requesting === true) {
      e.preventDefault();
    }
    // ACTIONS
    setLoading(true);
  };

 return (
  <div>
    
    <button onClick={onDebounce}>Requesting data</button>
  </div>
 )
}