如何在React中正确地进行回调应用调试和节流技术?

426 阅读5分钟

如何在React中正确地退避和节制回调

当React组件处理突发事件时,如窗口调整大小、滚动、用户输入输入等--明智的做法是软化这些事件的处理程序。

否则,当处理程序被频繁调用时,你就有可能使应用程序滞后,甚至几秒钟内没有反应。幸运的是,解压和节流技术可以帮助你控制事件处理程序的调用。

在这篇文章中,你将学习如何正确使用React钩子来对React中的回调应用调试和节流技术。

1.没有去重的回调

假设一个组件<FilterList> ,接受一个大的名字列表(至少有200条记录)。该组件有一个输入字段,用户在那里输入一个查询,然后根据该查询过滤名字。

这里是<FilterList> 组件的第一个版本:

import { useState } from 'react';
export function FilterList({ names }) {
  const [query, setQuery] = useState('');
  let filteredNames = names;
  if (query !== "") {
    filteredNames = names.filter((name) => {
      return name.toLowerCase().includes(query.toLowerCase());
    });
  }
  const changeHandler = event => {
    setQuery(event.target.value);
  };
  return (
    <div>
      <input 
        onChange={changeHandler} 
        type="text" 
        placeholder="Type a query..."
      />
      {filteredNames.map(name => <div key={name}>{name}</div>)}
    </div>
  );
}

当在输入框中输入查询时,你可以注意到列表中的每一个引入的字符都会被过滤掉。

例如,如果你一个字符一个字符地输入Michael ,那么该组件将显示闪烁的过滤列表,用于查询M,Mi,Mic,Mich,Micha,Michae,Michael 。然而,用户只需要看到一个过滤结果:单词Michael

让我们通过在changeHandler 回调函数上应用300ms 时间减压来改进过滤的过程。

2.对回调函数进行去抖,第一次尝试

为了对changeHandler 函数进行去抖,我将使用lodash.debounce 包。你可以随心所欲地使用任何其他库,甚至可以自己编写调试函数。

首先,我们来看看如何使用debounce()函数:

import debounce from 'lodash.debounce';
const debouncedCallback = debounce(callback, waitTime);

debounce() 函数接受callback 的参数函数,并返回该函数的去抖版本。

当debounced函数debouncedCallback 被多次调用时,在突发情况下,它将在最后一次调用后waitTime 过后才调用回调。

debouncing很适合软化<FilterList> 里面的过滤:让我们把changeHandler300ms 的等待时间进行debouncing。

对React组件内的changeHandler 应用debouncing的唯一问题是,该函数的debounced版本在组件重新渲染时应该保持不变。

第一种方法是使用useCallback(callback, dependencies) ,它可以在组件重新渲染之间保持一个被弹出函数的实例。

import { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';
export function FilterList({ names }) {
  const [query, setQuery] = useState("");
  let filteredNames = names;
  if (query !== "") {
    filteredNames = names.filter((name) => {
      return name.toLowerCase().includes(query.toLowerCase());
    });
  }
  const changeHandler = event => {
    setQuery(event.target.value);
  };
  const debouncedChangeHandler = useCallback(
    debounce(changeHandler, 300)
  , []);
  return (
    <div>
      <input 
        onChange={debouncedChangeHandler} 
        type="text" 
        placeholder="Type a query..."
      />
      {filteredNames.map(name => <div key={name}>{name}</div>)}
    </div>
  );
}

debounce(changeHandler, 300) 创建一个被处理的事件的贬值版本,而 ,确保在重新渲染之间返回同一个贬值回调实例。useCallback(debounce(changeHandler, 300), [])

注意:该方法也适用于创建节流的函数,例如:useCallback(throttle(callback, time), [])

打开演示,输入一个查询:你会看到,在最后一次输入后,列表被过滤,延迟时间为300ms :这带来了一个更柔和、更好的用户体验。

然而......这个实现有一个小的性能问题:每次组件重新渲染时,都会由debounce(changeHandler, 300) ,创建一个新的放行函数的实例。

这不是一个关于正确性的问题:useCallback() ,确保返回相同的去重函数实例。但明智的做法是避免在每次渲染时调用debounce(...)

让我们在下一节中看看如何避免在每次渲染时创建退避的函数。

3.退避回调,第二次尝试

幸运的是,使用useMemo() 钩子来替代useCallback() 是一个更理想的选择:

import { useState, useMemo } from 'react';
import debounce from 'lodash.debounce';
export function FilterList({ names }) {
  const [query, setQuery] = useState("");
  let filteredNames = names;
  if (query !== "") {
    filteredNames = names.filter((name) => {
      return name.toLowerCase().includes(query.toLowerCase());
    });
  }
  const changeHandler = (event) => {
    setQuery(event.target.value);
  };
  const debouncedChangeHandler = useMemo(
    () => debounce(changeHandler, 300)
  , []);
  return (
    <div>
      <input
        onChange={debouncedChangeHandler}
        type="text"
        placeholder="Type a query..."
      />
      {filteredNames.map(name => <div key={name}>{name}</div>)}
    </div>
  );
}

useMemo(() => debounce(changeHandler, 300), []) 备忘了debounced handler,但也只在组件的初始渲染过程中调用debounce()

这种方法也适用于创建节流的函数:useMemo(() => throttle(callback, time), [])

如果你打开这个演示,你会看到在输入框中输入的内容仍然是被取消的。

4.对依赖关系要小心

如果debounced handler使用props或state,为了避免产生陈旧的闭包,我建议正确设置useMemo() 的依赖关系。

import { useMemo } from 'react';
import debounce from 'lodash.debounce';
function MyComponent({ prop }) {
  const [value, setValue] = useState('');
  
  const eventHandler = () => {
    // the event uses `prop` and `value`
  };
  const debouncedEventHandler = useMemo(
    () => debounce(eventHandler, 300)
  , [prop, stateValue]);
  
  // ...
}

正确地设置依赖关系可以保证刷新debounced闭包。

5.清理工作

由于debouncing和throttling在执行函数时有一定的延迟,你可能会出现在组件卸载后执行函数的情况。

当不再需要时,建议取消debouncing和throttling。

退避和节流的实现通常提供一个特殊的方法来取消执行。例如lodash.debounce 库提供了debouncedCallback.cancel() 来取消任何预定的调用。

下面是当组件卸载时,你如何取消debounced函数:

import { useState, useMemo, useEffect } from 'react';
import debounce from 'lodash.debounce';
export function FilterList({ names }) {
  // ....
  const debouncedChangeHandler = useMemo(
    () => debounce(changeHandler, 300)
  , []);
  // Stop the invocation of the debounced function
  // after unmounting
  useEffect(() => {
    return () => {
      debouncedChangeHandler.cancel();
    }
  }, []);
  return (
    // ....
  );
}

6.总结

创建debounced和throttled函数,以处理经常发生的事件的一个好方法是使用useMemo()hook:

import { useMemo } from 'react';
import debounce from 'lodash.debounce';
import throttle from 'lodash.throttle';
function MyComponent() {
  const eventHandler = () => {
    // handle the event...
  };
  const debouncedEventHandler = useMemo(
    () => debounce(eventHandler, 300)
  , []);
  const throttledEventHandler = useMemo(
    () => throttle(eventHandler, 300)
  , []);
  
  // ...
}

如果放行或节流的事件处理程序访问道具或状态值,不要忘记设置useMemo(..., dependencies) 的依赖性参数。