使用debounce在react函数组件中做防抖
背景
在项目中有这样的常见需求:
提供一个搜索输入框,输入框需要响应用户的输入并且不需要用户点击搜索按钮直接发起搜索。
思路
1. 普通实现
其实这样的需求很常见,也并不难,第一反应就是监听用户输入,只要用户有输入操作立即拿到用户输入的数据直接发起搜索查询
-
优点:响应迅速,并且确实可以达到需求的效果。
-
缺点:发送多洗无效请求,浪费服务器的性能资源,增加服务器负担。
2. 防抖函数
使用防抖函数(debounce
或者lodash.debounce
)
- 优点:同样能做出迅速响应,并且能有效去除不必要的请求发送,节约服务器资源性能。
实现:
我手首先是将搜索框封装为一个组件MatSearchInput
import { Icon, InputBaseProps, StandardTextFieldProps, styled, TextField } from "@mui/material";
import { t } from "i18next";
import { memo } from "react";
const SytledInput = styled(TextField)(({ theme }) => {
return {
"& .MuiOutlinedInput-root": {
"& fieldset": {
borderColor: theme.palette.primary.main,
},
},
"&:hover fieldset": {
borderColor: theme.palette.primary.main + " !important",
},
};
});
interface MatSearchInputProps extends StandardTextFieldProps {
placeholder?: string;
// onChange?(): void;
width?: number;
inputProps?: InputBaseProps["inputProps"];
}
export default memo(function MatSearchInput(props: MatSearchInputProps) {
return (
<SytledInput
{...props}
style={{ width: props.width || 250 }}
InputProps={{
startAdornment: (
<Icon sx={{ mr: 1 }} color="primary">
search
</Icon>
),
}}
placeholder={props.placeholder ? t(props.placeholder) : "Search..."}
color="primary"
size="small"
></SytledInput>
);
});
然后在父级组件中监听input事件:
const fetchTemplate = useCallback(() => {
setLoading(true);
fetchData(new PageLink(page, pageSize, searchValue, sortProp, "desc")).then((res) => {
setTemplateData(res);
setLoading(false);
});
}, [page, sortProp, searchValue]);
useEffect(() => {
fetchTemplate();
}, [fetchTemplate]);
...
<MatSearchInput value={searchValue} onChange={(e) => setSearchValue(e.target.value)} sx={{ ml: 2 }}></MatSearchInput>
...
在这里fetchTemplate
为获取数据的方法,使用useCallback
进行缓存,也就是说当依赖searchValue
发生变化是该方法会重新计算,然后被useEffect
监听到并且调用。
然而现在的问题就是输入框组件只要触发onChange
事件就会直接引起请求发送,这是我们可以优化的点。
这里就用到了debounce
(或者lodash
库的debounce
方法)
3. 遇到的坑
最开始的实现思路是在输入框组件中用useState维护自己的value和onChange事件,这样用户输入的时候输入框才能正常工作而不会卡顿,然后在自己的onChange事件中触发props传来的onChange,代码如下:
export default memo(function MatSearchInput(props: MatSearchInputProps) {
const [value, setValue] = useState(props.value);
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
emitChangeByProps(event);
};
const emitChangeByProps = debounce(function (event: ChangeEvent<HTMLInputElement>) {
props.onChange(event);
}, 300);
return (
<SytledInput
{...props}
style={{ width: props.width || 250 }}
InputProps={{
startAdornment: (
<Icon sx={{ mr: 1 }} color="primary">
search
</Icon>
),
}}
placeholder={props.placeholder ? t(props.placeholder) : "Search..."}
color="primary"
size="small"
value={value}
onChange={onChange}
></SytledInput>
);
});
运行后没有预想中的效果
原因
思考后发现原因应该是之前说过的性能优化的问题,根据react函数式组件的特性,当useState存储的值value发生变化的时候,组件会重新渲染,因此emitChangeByProps
方法没次都是一个新的方法
解决
解决思路就是用useMemo将emitChangeByProps
方法缓存起来,这样就可以实现了:
export default memo(function MatSearchInput(props: MatSearchInputProps) {
const [value, setValue] = useState(props.value);
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
emitChangeByProps(event);
};
// 这里用debounce做防抖 做性能优化
const emitChangeByProps = useMemo(() => {
return debounce(function (event: ChangeEvent<HTMLInputElement>) {
props.onChange(event);
}, 300);
}, [props]);
return (
<SytledInput
{...props}
style={{ width: props.width || 250 }}
InputProps={{
startAdornment: (
<Icon sx={{ mr: 1 }} color="primary">
search
</Icon>
),
}}
placeholder={props.placeholder ? t(props.placeholder) : "Search..."}
color="primary"
size="small"
value={value}
onChange={onChange}
></SytledInput>
);
});