如何在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> 里面的过滤:让我们把changeHandler 到300ms 的等待时间进行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) 的依赖性参数。