react解决input未失焦立即点保存,无法拿到计算后的数据问题

16 阅读2分钟

背景

  1. 项目比较复杂,涉及大批量数据录入和计算,onChage事件有性能问题,而采用onblur作为数据计算时机
  2. 用户习惯戏录入最后一个数据时,直接点保存按钮,而此时oblur回调还没有执行完,也就是最后一个录入的数据要触发的变更没有能拿到

解决方案

  1. 将input/inputNumber组件onblur事件包装一层
  2. 包装的onblur事件接受不了一个promise funciton
  3. onblur触发时,将promise 收集起来
  4. 提供一个wrapper,包裹save 代码
  5. 触发onsave时,等待所有的promise结束后,执行save方法

代码方案

  1. util.ts
import { useMemoizedFn } from "ahooks";
import { FormInstance } from "antd";
import makeDeferred from "./deferred";
import { flushSync } from "react-dom";

// 异步队列
let taskPromise: Array<Promise<any>> = [];

/**
 * 失焦返回的所有Promise
 * @returns Promise<any>
 */
const globalBlurPromise = async () => {
  const tasks = await Promise.all(taskPromise);
  taskPromise = [];
  return tasks;
};

/**
 * 包裹onblur
 */
export const useAsyncDelayer = (
  fn: (e: React.FocusEvent<HTMLInputElement, Element>) => Promise<any>,
) => {
  const run = useMemoizedFn((e) => {
    const task = fn?.(e);
    taskPromise.push(task);
    return task;
  });

  return { run };
};

// 处理promise tasks
const deferAsync = async (form?: FormInstance) => {
  const defer = makeDeferred<boolean>();
  let unregister: any;
  // 使用form且有task要执行,没有task的时候,callback不会触发
  if (form && !!taskPromise?.length) {
    // 等待form 更新结束
    const callback = () => {
      defer.resolve(true);
    };
    unregister = (form as any)
      .getInternalHooks("RC_FORM_INTERNAL_HOOKS")
      .registerWatch(callback);
  }
  await globalBlurPromise();
  if (!form) {
    // 强制react更新已提交的update
    flushSync(() => {});
  }
  defer.resolve(true);
  await defer.promise;
  return unregister?.();
};

// 提供onsave 包裹器
export const useBlurSave = (
  fn: (...args: any) => any,
  form?: FormInstance<any>,
) => {
  const memoFn = useMemoizedFn(fn);
  const saveFn = useMemoizedFn(async (...args) => {
    // 等待所有的promise结束
    await deferAsync(form);
    await memoFn(...args);
  });
  return saveFn;
};

  1. deferred.ts
interface IDeferred<T> {
  resolve: (value: T) => void;
  reject: (reason: any) => void;
  promise: Promise<T>;
}

function makeDeferred<T>() {
  const deferred = {} as IDeferred<T>;
  const promise = new Promise<T>((resolve, reject) => {
    deferred.resolve = resolve;
    deferred.reject = reject;
  });
  deferred.promise = promise;
  return deferred;
}

export default makeDeferred;
  1. AsyncInputNumber.tsx
import { InputNumber, InputNumberProps } from "antd";
import { useAsyncDelayer } from "./utils";

const AsyncInputNumber = ({
  onBlur,
  ...restProps
}: InputNumberProps & {
  onBlur: (e: React.FocusEvent<HTMLInputElement, Element>) => Promise<any>;
}) => {
  const { run } = useAsyncDelayer(onBlur);

  return <InputNumber onBlur={(e) => run(e)} {...restProps} />;
};

export default AsyncInputNumber;

  1. 示例
import type { FormProps } from "antd";
import { Button, Divider, Form, InputNumber } from "antd";
import makeDeferred from "./deferred";
import { useBlurSave } from "./utils";

type FieldType = {
  count?: string;
  price?: string;
  amount?: string;
};

const onFinishFailed: FormProps<FieldType>["onFinishFailed"] = (errorInfo) => {
  console.log("Failed:", errorInfo);
};

function OnblurDemo() {
  const [form] = Form.useForm();
  const [asyncForm] = Form.useForm();

  const handleSubmit = () => {
    console.log("=====sync", form.getFieldsValue());
  };

  const handleBlur = async () => {
    const values = form.getFieldsValue();
    const defer = makeDeferred();
    setTimeout(() => {
      defer.resolve(true);
    }, 300);
    await defer.promise;
    form.setFieldValue("amount", (values.count || 0) * (values.price || 0));
  };

  const handleAsyncBlur = async () => {
    const values = asyncForm.getFieldsValue();
    const defer = makeDeferred();
    setTimeout(() => {
      defer.resolve(true);
    }, 300);
    await defer.promise;
    asyncForm.setFieldValue(
      "amount",
      (values.count || 0) * (values.price || 0),
    );
  };

  const handleAsyncSubmit = useBlurSave(() => {
    // 拿到异步后的值
    console.log("=====async", form.getFieldsValue());
  });

  return (
    <div>
      <Form
        name="basic"
        form={form}
        labelCol={{ span: 8 }}
        wrapperCol={{ span: 16 }}
        style={{ maxWidth: 600 }}
        onFinishFailed={onFinishFailed}
        autoComplete="off"
      >
        <Form.Item<FieldType>
          label="数量"
          name="count"
          rules={[{ required: true, message: "Please input your count!" }]}
        >
          <InputNumber onBlur={handleBlur} />
        </Form.Item>

        <Form.Item<FieldType>
          label="单价"
          name="price"
          rules={[{ required: true, message: "Please input your price!" }]}
        >
          <InputNumber onBlur={handleBlur} />
        </Form.Item>

        <Form.Item<FieldType> label="总计" name="amount">
          <InputNumber disabled />
        </Form.Item>

        <Button type="primary" onClick={handleSubmit}>
          Submit
        </Button>
      </Form>
      <Divider />
      <Form
        name="basic"
        form={asyncForm}
        labelCol={{ span: 8 }}
        wrapperCol={{ span: 16 }}
        style={{ maxWidth: 600 }}
        onFinishFailed={onFinishFailed}
        autoComplete="off"
      >
        <Form.Item<FieldType>
          label="数量"
          name="count"
          rules={[{ required: true, message: "Please input your count!" }]}
        >
          <InputNumber onBlur={handleAsyncBlur} />
        </Form.Item>

        <Form.Item<FieldType>
          label="单价"
          name="price"
          rules={[{ required: true, message: "Please input your price!" }]}
        >
          <InputNumber onBlur={handleAsyncBlur} />
        </Form.Item>

        <Form.Item<FieldType> label="总计" name="amount">
          <InputNumber disabled />
        </Form.Item>

        <Button type="primary" onClick={handleAsyncSubmit}>
          Submit
        </Button>
      </Form>
    </div>
  );
}

export default OnblurDemo;