fix: Select组件开启autoComplete后请求竞态问题 Close: #8817

This commit is contained in:
lurunze1226 2023-11-28 20:24:04 +08:00
parent 279f876f84
commit 4c16138005
5 changed files with 106 additions and 21 deletions

View File

@ -1199,7 +1199,7 @@ leftOptions 动态加载,默认 source 接口是返回 options 部分,而 le
"name": "select1",
"type": "select",
"label": "选项自动补全(单选)",
"autoComplete": "/api/mock2/options/autoComplete?term=${term}",
"autoComplete": "/api/mock2/options/autoComplete3?delay=true&term=${term}",
"placeholder": "请输入",
"clearable": true
},

View File

@ -0,0 +1,60 @@
/**
* @file 请求随机延迟的模拟接口
*/
function generateRandomNames(num) {
const candidate = ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank', 'Grace', 'Heidi', 'Ivan', 'Judy', 'Mike', 'Nina', 'Oliver', 'Polly', 'Queenie', 'Randy', 'Sybil', 'Trudy', 'Victor', 'Wendy', 'Xander', 'Yvonne', 'Zoe'];
const randomNames = [];
for (let i = 0; i < num; i++) {
const randomIndex = Math.floor(Math.random() * candidate.length);
randomNames.push(candidate[randomIndex]);
}
return [...new Set(randomNames)].map(i => ({ value: i, label: i }));
}
function generateRandomDelay() {
const timeout = [0, 200, 500, 700, 1000, 2000];
const randomDelay = [];
for (let i = 0; i < timeout.length; i++) {
const randomIndex = Math.floor(Math.random() * timeout.length);
randomDelay.push(timeout[randomIndex]);
}
return randomDelay[0];
}
function isPositiveInteger(input) {
var pattern = /^\d+$/;
return pattern.test(input);
}
module.exports = function (req, res) {
const labelField = req.query.labelField || 'label';
const valueField = req.query.valueField || 'value';
const term = req.query.term || '';
const useDelay = req.query.delay || false;
const total = isPositiveInteger(Number(req.query.total)) ? Number(req.query.total) : 20;
const list = generateRandomNames(total).map(item => ({[labelField]: item.label, [valueField]: item.value}));
const delay = generateRandomDelay();
const responseWrapper = () => {
res.json({
status: 0,
msg: '',
data: term
? list.filter(function (item) {
return term ? ~item.label.toLowerCase().indexOf(term.toLowerCase()) : false;
})
: list
});
}
if (useDelay) {
return setTimeout(responseWrapper, delay);
}
return responseWrapper();
};

View File

@ -32,6 +32,7 @@ export const Store = types
})
.actions(self => {
let component: any = undefined;
let fetchCancel: Function | null = null;
const load = flow(function* (
env: RendererEnv,
@ -40,8 +41,22 @@ export const Store = types
config: WithRemoteConfigSettings = {}
): any {
try {
if (fetchCancel) {
fetchCancel?.('remote load request cancelled.');
fetchCancel = null;
self.fetching = false;
}
if (self.fetching) {
return;
}
self.fetching = true;
const ret: Payload = yield env.fetcher(api, ctx);
const ret: Payload = yield env.fetcher(api, ctx, {
cancelExecutor: (executor: Function) => (fetchCancel = executor)
});
fetchCancel = null;
if (!isAlive(self)) {
return;
}
@ -202,7 +217,6 @@ export function withRemoteConfig<P = any>(
ComposedComponent as React.ComponentType<T>;
static contextType = EnvContext;
toDispose: Array<() => void> = [];
loadOptions = debounce(this.loadAutoComplete.bind(this), 250, {
trailing: true,
leading: false

View File

@ -931,7 +931,6 @@ export default class NestedSelectControl extends React.Component<
translate: __,
inline,
searchable,
autoComplete,
selectedOptions,
clearable,
loading,

View File

@ -1,29 +1,28 @@
import React from 'react';
import cx from 'classnames';
import find from 'lodash/find';
import debounce from 'lodash/debounce';
import {
OptionsControl,
OptionsControlProps,
Option,
FormOptionsControl,
resolveEventData,
str2function
str2function,
Api,
ActionObject,
normalizeOptions,
isEffectiveApi,
isApiOutdated,
createObject,
autobind
} from 'amis-core';
import {normalizeOptions} from 'amis-core';
import find from 'lodash/find';
import debouce from 'lodash/debounce';
import {Api, ActionObject} from 'amis-core';
import {isEffectiveApi, isApiOutdated} from 'amis-core';
import {isEmpty, createObject, autobind, isMobile} from 'amis-core';
import {TransferDropDown, Spinner, Select, SpinnerExtraProps} from 'amis-ui';
import {FormOptionsSchema, SchemaApi} from '../../Schema';
import {Spinner, Select, SpinnerExtraProps} from 'amis-ui';
import {BaseTransferRenderer, TransferControlSchema} from './Transfer';
import {TransferDropDown} from 'amis-ui';
import {supportStatic} from './StaticHoc';
import type {SchemaClassName} from '../../Schema';
import type {TooltipObject} from 'amis-ui/lib/components/TooltipWrapper';
import type {PopOverOverlay} from 'amis-ui/lib/components/PopOverContainer';
import {supportStatic} from './StaticHoc';
/**
* Select
@ -191,7 +190,7 @@ export default class SelectControl extends React.Component<SelectProps, any> {
super(props);
this.changeValue = this.changeValue.bind(this);
this.lazyloadRemote = debouce(this.loadRemote.bind(this), 250, {
this.lazyloadRemote = debounce(this.loadRemote.bind(this), 250, {
trailing: true,
leading: false
});
@ -216,6 +215,7 @@ export default class SelectControl extends React.Component<SelectProps, any> {
componentWillUnmount() {
this.unHook && this.unHook();
this.fetchCancel = null;
}
inputRef(ref: any) {
@ -324,6 +324,8 @@ export default class SelectControl extends React.Component<SelectProps, any> {
onChange?.(newValue);
}
fetchCancel: Function | null = null;
async loadRemote(input: string) {
const {
autoComplete,
@ -356,12 +358,22 @@ export default class SelectControl extends React.Component<SelectProps, any> {
});
}
if (this.fetchCancel) {
this.fetchCancel?.('autoComplete request cancelled.');
this.fetchCancel = null;
setLoading(false);
}
setLoading(true);
try {
const ret = await env.fetcher(autoComplete, ctx);
const ret = await env.fetcher(autoComplete, ctx, {
cancelExecutor: (executor: Function) => (this.fetchCancel = executor)
});
this.fetchCancel = null;
const options = (ret.data && (ret.data as any).options) || ret.data || [];
const combinedOptions = this.mergeOptions(options);
let options = (ret.data && (ret.data as any).options) || ret.data || [];
let combinedOptions = this.mergeOptions(options);
setOptions(combinedOptions);
return {