前言
签约作者写的就是漂亮,配上源码和动图,让人一看就感觉靠谱,但是呢,我还是没有看太懂,所以,我自己也模拟实现了一个 useDeferredValue 版本。 该作者使用的是 React ,而我使用 React-Native。由于,由于项目任务工期重,没有时间看源码,所以,自己按照自己的理解设计和现实。后续,我再找源码看看。如果先看了源码,可能就会干扰到自己的设计思路了,有利有弊。
典型应用场景
搜索长列表,结果非常多,每输入一个字符就进行一次搜索查找就非常消耗资源,所以,如果有一个延迟反馈的工具,就可以让搜索不会那么频繁执行,从用户角度说连续搜索表明当前并不想进行查询。
设计思路
搜索框,本质就是输入框的改良版本。在 react-native 中,TextInput 进行稍微的修改就可以变身成搜索框。通过 onChangeText 和 value 进行输入字符变更通知和设置。
所以,设计入口在这里:让输入的值慢一点的生效,在输入间隔时间短的时候等待下一次有效字符的输入在进行搜索。
核心: 采用定时器 NodeJS.Timeout 作为延迟技术, 使用 useRef,useMemo和 useCallback 缓存数据和方法。
标准: 间隔时间短的输入认为无效请求,超时之后才是真正生效请求。
接口: 和官方保持一致,不引入额外参数或者改变接口。
具体实现
const DEFER = 500;
type TimeoutType = {
timeoutId: NodeJS.Timeout;
cur: number;
};
/**
* ref: https://juejin.cn/post/7083466010505773093#3
* !!! This function does not exist before react 18.
*
* @param value any type value
* @param defer timeout (ms)
* @returns deferred timeout value
*/
export const useDeferredValue = <T,>(value: T, defer: number = DEFER) => {
const v = versionToArray(React.version);
if (v[0] && v[0] >= 18) {
throw new Error('Please use the official version.');
}
const _preValue = React.useRef(value);
const preValue = React.useMemo(() => {
return _preValue;
}, []);
const [_value, setValue] = React.useState(preValue.current);
const _timeout = React.useRef<TimeoutType>();
const timeout = React.useMemo(() => {
return _timeout;
}, []);
const _create = React.useCallback(
(
defer: number,
timeout: React.MutableRefObject<TimeoutType | undefined>,
dispatch: React.Dispatch<React.SetStateAction<T>>,
value: T,
preValue: React.MutableRefObject<T>
) => {
if (timeout.current === undefined) {
timeout.current = {
timeoutId: setTimeout(() => {
preValue.current = value;
timeout.current = undefined;
dispatch(value);
}, defer),
cur: new Date().getTime(),
};
}
},
[]
);
const _cancel = React.useCallback(
(
defer: number,
timeout: React.MutableRefObject<TimeoutType | undefined>
) => {
if (timeout.current) {
const cur = new Date().getTime();
if (cur <= timeout.current.cur + defer) {
clearTimeout(timeout.current.timeoutId);
timeout.current = undefined;
}
}
},
[]
);
if (preValue.current === value) {
return _value;
}
_cancel(defer, timeout);
_create(defer, timeout, setValue, value, preValue);
return _value;
};
测试代码
import * as React from 'react';
import { Button as RNButton, Text, View } from 'react-native';
import { useDeferredValue } from 'react-native-chat-uikit';
let count = 0;
export default function TestUtil() {
React.useEffect(() => {}, []);
const [value, setValue] = React.useState(0);
const [value2, setValue2] = React.useState(0);
const useDeferredValueM = React.useCallback(useDeferredValue, [value]);
type ObjType = {
name: string;
age: number;
};
const [obj, setObj] = React.useState<ObjType>({ name: 'zs', age: count });
return (
<View style={{ marginTop: 100 }}>
<View>
<RNButton
title="compare value"
onPress={() => {
setValue(++count);
}}
>
defer value
</RNButton>
<Text>{value}</Text>
<Text>{useDeferredValue(value)}</Text>
</View>
<View>
<RNButton
title="compare value"
onPress={() => {
setValue2(++count);
}}
>
defer value2
</RNButton>
<Text>{value2}</Text>
<Text>{useDeferredValueM(value2)}</Text>
</View>
<View>
<RNButton
title="compare object value"
onPress={() => {
setObj({ name: obj.name, age: obj.age + 1 });
}}
>
defer object value
</RNButton>
<Text>{obj.age}</Text>
<Text>{useDeferredValueM(obj).age}</Text>
</View>
</View>
);
}
实现效果
说明
有兴趣的小伙伴可能对实现里面的内容有兴趣,可亲自尝试和验证,每一句代码都是认真的。
源码
2023.02.15 更新
上面的实现没有问题,但是限制较多,在 callback 方法里面无法使用。所以,有了 改进 方法。
实现思路和原来的一致,区别就是在变量的保存上没有使用 use 系列。
// ref: https://github.com/jashkenas/underscore/blob/b713f5a6d75b12c8c57fb3f410df029497c2a43f/modules/throttle.js
type Function = (...args: any[]) => any;
// A (possibly faster) way to get the current timestamp as an integer.
function now() {
return new Date().getTime();
}
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
export function throttle(
func: Function,
wait: number = 500,
options: any = { leading: false }
) {
let timeout: NodeJS.Timeout | null, context: any, args: any, result: any;
let previous = 0;
if (!options) options = {};
const later = function () {
previous = options.leading === false ? 0 : now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
const throttled = function (...argument: any[]) {
const _now = now();
if (!previous && options.leading === false) previous = _now;
const remaining = wait - (_now - previous);
context = throttled;
args = argument;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = _now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
throttled.cancel = function () {
if (timeout) {
clearTimeout(timeout);
}
previous = 0;
timeout = context = args = null;
};
return throttled;
}