在react函数式组件中使用防抖与节流函数

414 阅读2分钟

先来看一段代码:

import debounce from 'lodash/debounce';

export default class Search extends Component {
    constructor(props) {
      super(props)
      this.handleSearch = debounce(this.handleSearch, 500);
    }
    handleSubmit = (e) => {
        e.preventDefault();
        this.handleSearch();
    }
    handleSearch = () => {
    ...
    }
    render() {
        return (
            <form onSubmit={this.handleSubmit}><form>
        )
    }
}

这是一段在react类组件中使用节流函数的示例代码,而当你想在函数式组件中使用节流函数时,你可能会自然而然的想要这么做::

import React, {useState} from "react";
import debounce from 'lodash/debounce';
const sendQuery = (query) => console.log(`Querying for ${query}`);
const Search = () => {
  const [userQuery, setUserQuery] = useState("");
  const delayedQuery = debounce(q => sendQuery(q), 500);
  const onChange = e => {
    setUserQuery(e.target.value);
    delayedQuery(e.target.value);
  };
  return (
    <div>
      <label>Search:</label>
      <input onChange={onChange} value={userQuery} />
    </div>
  );
}

但这样子做防抖函数并没有发挥作用,仅仅是把反应时间延后了500毫秒。这是因为函数时组件每次渲染结束之后,内部的变量都会被释放,重新渲染时所有的变量会被重新初始化,产生的结果就是每一次都注册和执行了setTimeout函数。想要得到正确的运行结果,必须以某种方式存储那些本会被删除的变量和方法的引用。很遗憾,没办法直接使用useState去存储,但是我们可以直接构造一个新的hook去解决这个问题。

import React, { useState, useEffect } from 'react';

// Our hook
export default function useDebounce(value, delay) {
  // State and setters for debounced value
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(
    () => {
      // Set debouncedValue to value (passed in) after the specified delay
      const handler = setTimeout(() => {
        setDebouncedValue(value);
      }, delay);

      // Return a cleanup function that will be called every time ...
      // ... useEffect is re-called. useEffect will only be re-called ...
      // ... if value changes (see the inputs array below). 
      // This is how we prevent debouncedValue from changing if value is ...
      // ... changed within the delay period. Timeout gets cleared and restarted.
      // To put it in context, if the user is typing within our app's ...
      // ... search box, we don't want the debouncedValue to update until ...
      // ... they've stopped typing for more than 500ms.
      return () => {
        clearTimeout(handler);
      };
    },
    // Only re-call effect if value changes
    // You could also add the "delay" var to inputs array if you ...
    // ... need to be able to change that dynamically.
    [value] 
  );

  return debouncedValue;
}

构造完useDebounce函数后,我们可以这样使用:

import React, { useState, useEffect } from 'react';
import useDebounce from './use-debounce';

const sendQuery = (query) => console.log(`Querying for ${query}`);
// Usage
function Search() {
  // State and setter for search term
  const [searchTerm, setSearchTerm] = useState('');
  // State and setter for search results
  const [results, setResults] = useState([]);
  // State for search status (whether there is a pending API request)
  const [isSearching, setIsSearching] = useState(false);

  // Now we call our hook, passing in the current searchTerm value.
  // The hook will only return the latest value (what we passed in) ...
  // ... if it's been more than 500ms since it was last called.
  // Otherwise, it will return the previous value of searchTerm.
  // The goal is to only have the API call fire when user stops typing ...
  // ... so that we aren't hitting our API rapidly.
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  // Here's where the API call happens
  // We use useEffect since this is an asynchronous action
  useEffect(
    () => {
      // Make sure we have a value (user has entered something in input)
      if (debouncedSearchTerm) {
        // Set isSearching state
        setIsSearching(true);
        // Fire off our API call
        sendQuery(debouncedSearchTerm);
        // Set back to false since request finished
        setIsSearching(false);
        // Set results state
        setResults(results);
      } else {
        setResults([]);
      }
    },
    // This is the useEffect input array
    // Our useEffect function will only execute if this value changes ...
    // ... and thanks to our hook it will only change if the original ...
    // value (searchTerm) hasn't changed for more than 500ms.
    [debouncedSearchTerm]
  );

  return (
    <div>
      <label>Search:</label>
      <input onChange={e => setSearchTerm(e.target.value) />
    </div>
  );
}

上面的方法还是太复杂了,通过使用useRef和useCallback我们可以更简单的去解决这个问题:

const SearchFixed = () => {
  const [userQuery, setUserQuery] = useState("");
  const delayedQuery = useRef(debounce(q => sendQuery(q), 500)).current;
  const onChange = e => {
    setUserQuery(e.target.value);
    delayedQuery(e.target.value);
  };
  return (
    <div>
      <label>Search Fixed:</label>
      <input onChange={onChange} value={userQuery} />
    </div>
  );
};

或是:

const SearchFixed = () => {
  const [userQuery, setUserQuery] = useState("");
  const delayedQuery = useCallback(debounce(q => sendQuery(q), 500), []);
  const onChange = e => {
    setUserQuery(e.target.value);
    delayedQuery(e.target.value);
  };
  return (
    <div>
      <label>Search Fixed:</label>
      <input onChange={onChange} value={userQuery} />
    </div>
  );
};



参考资料:

  1. dev.to/gabe_raglan…
  2. medium.com/@kartikag01…
  3. medium.com/@rajeshnaro…