输入框数据丢失问题
输入框、IME组合输入和防抖
项目中存在大量输入框,需要实现onChange时进行接口请求.
现在项目中的输入框会使用debounce来避免持续输入时的频繁请求,但有时还会因为debounce丢失最后的中文输入
可以通过compositionstart、compositionupdate、compositionend来控制输入框,替换掉debounce的逻辑
参考链接:
mdn文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Element/compositionstart_event
具体实现:https://juejin.cn/post/7313242107682340890
封装了新的input组件库,博客中不方便展示,就简单说明一下实现的内容吧
1. IME 组合输入优化:
- compositionstart 期间暂停触发 onChange
- compositionend 时立即回调一次最终内容
2. 受控/非受控模式:
- 通过 value 与 onChange 实现受控
- 不传 value 可作为非受控输入框使用
3. 完整继承 Antd Input的各种类型:
- 参数variant支持'Search' || 'TextArea' || 'Password' || 什么都不传就是默认input
- 除 onChange 以外的所有原生 props 原样透传
- e.g.主题、尺寸、后缀按钮等等
4.内置debounce:
- 默认设置了300ms的debounce,不用手动重复添加了
- 参数debounce也对外开放.
composition解决了中文输入的问题,其实是一个取巧的方式。
在快速删除输入框内容后,或者在loading时期快速输入,都会出现请求丢失的问题。
这种情况就无法通过composition解决了,需要直面问题的核心
那为什么请求会丢失?(压根没发出去)
按照这个例子来看:

我们输入一个完整的字符串'jintianzhongwuchishenme',然后逐渐删除。可以发现最后发出的一个请求的查询参数是'j',但是此时input输入框中是空的,没有这个''查询参数的请求
先看代码侧发生了什么:

每一次seachValue变化时,都会new一个全新的debounce,同一份debounce内,300ms之中只会执行一次,但是反复new新的debounce,等于在队列中排了多个定时器,旧的没触发新的定时器又加进来了。
接下来看下protable的源码发生了什么


/**
* 该函数用于进行数据请求,可以用于轮询或单次请求。
* 通过使用 AbortController 取消之前的请求,避免出现请求堆积。
* 若需要轮询,则在一定时间后再次调用该函数,最小时间为 200ms,避免一直处于 loading 状态。
* 如果请求被取消,则返回空。
*/
const fetchListDebounce = useDebounceFn(async (isPolling: boolean) => {
if (pollingSetTimeRef.current) {
clearTimeout(pollingSetTimeRef.current);
}
if (!getData) {
return;
}
const abort = new AbortController();
abortRef.current = abort;
try {
/**
* 这段代码使用了 Promise.race,同时发起了两个异步请求。
* fetchList 函数发起一个数据请求,而第二个 Promise 是等待通过 AbortSignal 取得一个信号。
* 如果第二个 Promise 得到了一个 AbortSignal 的信号,就会触发 reject,Promise.race 的结果也会结束。
* 这样,就达到了取消请求的目的。如果 fetchList 函数先返回了结果,那么该结果就是 Promise.race 的结果,
* 此时第二个 Promise 就会被取消。
*/
const msg = (await Promise.race([
fetchList(isPolling),
new Promise((_, reject) => {
abortRef.current?.signal?.addEventListener?.('abort', () => {
reject('aborted');
// 结束请求,并且清空loading控制
fetchListDebounce.cancel();
requestFinally();
});
}),
])) as DataSource[];
if (abort.signal.aborted) return;
// 放到请求前面会导致数据是上一次的
const needPolling = runFunction(polling, msg);
/*
* 这段代码是用于控制轮询的。其中,needPolling 参数表明当前是否需要进行轮询,umountRef 是一个 ref,用来记录组件是否被卸载。
* 如果需要轮询并且组件没有被卸载,就会调用 setTimeout,等待一定的时间,然后再次调用 fetchListDebounce 函数,并传入需要轮询的时间参数。
* 其中 Math.max(needPolling, 2000) 用于确定最小的轮询时间为 2000ms,避免频繁请求导致一直处于 loading 状态。
*/
if (needPolling && !umountRef.current) {
pollingSetTimeRef.current = setTimeout(
() => {
fetchListDebounce.run(needPolling);
// 这里判断最小要2000ms,不然一直loading
},
Math.max(needPolling, 2000),
);
}
return msg;
} catch (error) {
if (error === 'aborted') {
return;
}
throw error;
}
}, debounceTime || 30);
reloadAndRest会abortFetch(),把仍然在排队的后续reload全部取消,可以看到源码中的fetchListDebounce.cancel()以及那个promise.race().
解决思路
对libraryTable进行各种修改后都无法根绝请求丢失的问题,所以只能升级protable来解决
为什么能解决? 项目版本(2.7)的reload-->

新版本(latest)的reload-->

版本diff
旧版本reload()先执行abortFetch(),取消当前请求,然后调用fetchListDebounce.run(false)。此时manualRequestRef.current仍然保持为true,fetchList()检测到这个ref为true后直接return。表格此时仍然处于loading状态,此时在input输入框输入的所有请求都会被abortFetch()连带取消掉,导致请求压根没发到后端。
新版本会将manualRequestRef.current设置为false,然后再执行 fetchListDebounce.run(false),新的请求得以正常发送,abortFetch() 仍然只负责取消上一条未完成的请求。 避免loading时期请求被abort的问题