背景
- 项目比较复杂,涉及大批量数据录入和计算,onChage事件有性能问题,而采用onblur作为数据计算时机
- 用户习惯戏录入最后一个数据时,直接点保存按钮,而此时oblur回调还没有执行完,也就是最后一个录入的数据要触发的变更没有能拿到
解决方案
- 将input/inputNumber组件onblur事件包装一层
- 包装的onblur事件接受不了一个promise funciton
- onblur触发时,将promise 收集起来
- 提供一个wrapper,包裹save 代码
- 触发onsave时,等待所有的promise结束后,执行save方法
代码方案
- util.ts
import { useMemoizedFn } from "ahooks";
import { FormInstance } from "antd";
import makeDeferred from "./deferred";
import { flushSync } from "react-dom";
let taskPromise: Array<Promise<any>> = [];
const globalBlurPromise = async () => {
const tasks = await Promise.all(taskPromise);
taskPromise = [];
return tasks;
};
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 };
};
const deferAsync = async (form?: FormInstance) => {
const defer = makeDeferred<boolean>();
let unregister: any;
if (form && !!taskPromise?.length) {
const callback = () => {
defer.resolve(true);
};
unregister = (form as any)
.getInternalHooks("RC_FORM_INTERNAL_HOOKS")
.registerWatch(callback);
}
await globalBlurPromise();
if (!form) {
flushSync(() => {});
}
defer.resolve(true);
await defer.promise;
return unregister?.();
};
export const useBlurSave = (
fn: (...args: any) => any,
form?: FormInstance<any>,
) => {
const memoFn = useMemoizedFn(fn);
const saveFn = useMemoizedFn(async (...args) => {
await deferAsync(form);
await memoFn(...args);
});
return saveFn;
};
- 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;
- 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;
- 示例
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;