技术

输入框数据丢失问题

输入框、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的问题

仓库链接:https://github.com/ant-design/pro-components