fix:修复事件动作可能导致循环依赖问题 (#4083)

This commit is contained in:
hsm-lv 2022-04-19 15:31:35 +08:00 committed by GitHub
parent d1d1eddf5d
commit 0a1d1a10d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 164 additions and 186 deletions

View File

@ -20,7 +20,7 @@ import getExprProperties from './utils/filter-schema';
import {anyChanged, chainEvents, autobind} from './utils/helper';
import {SimpleMap} from './utils/SimpleMap';
import type {RendererEvent} from './utils/renderer-event';
import {bindEvent, dispatchEvent, RendererEvent} from './utils/renderer-event';
import {isAlive} from 'mobx-state-tree';
import {reaction} from 'mobx';
import {resolveVariableAndFilter} from './utils/tpl-builtin';
@ -93,9 +93,8 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
}
componentDidMount() {
const {env} = this.props;
// 这里无法区分监听的是不是广播所以又bind一下主要是为了绑广播
this.unbindEvent = env.bindEvent(this.cRef);
this.unbindEvent = bindEvent(this.cRef);
}
componentWillUnmount() {
@ -206,8 +205,8 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
async dispatchEvent(
e: React.MouseEvent<any>,
data: any
): Promise<RendererEvent<any> | undefined> {
return await this.props.env.dispatchEvent(e, this.cRef, this.context, data);
): Promise<RendererEvent<any> | void> {
return await dispatchEvent(e, this.cRef, this.context, data);
}
renderChild(

View File

@ -1,3 +1,4 @@
import {RendererProps} from '../factory';
import {extendObject} from '../utils/helper';
import {RendererEvent} from '../utils/renderer-event';
import {evalExpression} from '../utils/tpl';
@ -29,26 +30,26 @@ export interface LogicAction extends ListenerAction {
children?: ListenerAction[]; // 子动作
}
export interface ListenerContext {
export interface ListenerContext extends React.Component<RendererProps> {
[propName: string]: any;
}
// Action 基础接口
export interface Action {
export interface RendererAction {
// 运行这个 Action每个类型的 Action 都只有一个实例run 函数是个可重入的函数
run: (
action: ListenerAction,
renderer: ListenerContext,
event: RendererEvent<any>,
mergeData?: any // 有些Action内部需要通过上下文数据处理专有逻辑这里的数据是事件数据+渲染器数据
) => Promise<void>;
) => Promise<RendererEvent<any> | void>;
}
// 存储 Action 和类型的映射关系,用于后续查找
const ActionTypeMap: {[key: string]: Action} = {};
const ActionTypeMap: {[key: string]: RendererAction} = {};
// 注册 Action
export const registerAction = (type: string, action: Action) => {
export const registerAction = (type: string, action: RendererAction) => {
ActionTypeMap[type] = action;
};
@ -97,7 +98,7 @@ export const runActions = async (
// 执行动作,与原有动作处理打通
export const runAction = async (
actionInstrance: Action,
actionInstrance: RendererAction,
actionConfig: ListenerAction,
renderer: ListenerContext,
event: any

View File

@ -5,7 +5,7 @@ import {createObject, isEmpty, isVisible} from '../utils/helper';
import {RendererEvent} from '../utils/renderer-event';
import {filter} from '../utils/tpl';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
registerAction
@ -27,7 +27,7 @@ export interface IAjaxAction extends ListenerAction {
* @class AjaxAction
* @implements {Action}
*/
export class AjaxAction implements Action {
export class AjaxAction implements RendererAction {
async run(
action: IAjaxAction,
renderer: ListenerContext,

View File

@ -1,6 +1,6 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
LoopStatus,
@ -14,7 +14,7 @@ import {
* @class BreakAction
* @implements {Action}
*/
export class BreakAction implements Action {
export class BreakAction implements RendererAction {
async run(
action: ListenerAction,
renderer: ListenerContext,

View File

@ -1,7 +1,8 @@
import {RendererProps} from '../factory';
import {createObject} from '../utils/helper';
import {RendererEvent} from '../utils/renderer-event';
import {RendererEvent, dispatchEvent} from '../utils/renderer-event';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
registerAction
@ -18,7 +19,7 @@ export interface IBroadcastAction extends ListenerAction {
* @class BroadcastAction
* @implements {Action}
*/
export class BroadcastAction implements Action {
export class BroadcastAction implements RendererAction {
async run(
action: IBroadcastAction,
renderer: ListenerContext,
@ -33,7 +34,7 @@ export class BroadcastAction implements Action {
event.setData(createObject(event.data, action.args));
// 直接触发对应的动作
return await event.context.env.dispatchEvent(
return await dispatchEvent(
action.eventName,
renderer,
event.context.scoped,

View File

@ -1,7 +1,7 @@
import {RendererEvent} from '../utils/renderer-event';
import {dataMapping} from '../utils/tpl-builtin';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
LoopStatus,
@ -19,7 +19,7 @@ export interface ICmptAction extends ListenerAction {
* @class CmptAction
* @implements {Action}
*/
export class CmptAction implements Action {
export class CmptAction implements RendererAction {
async run(
action: ICmptAction,
renderer: ListenerContext,

View File

@ -1,6 +1,6 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
LoopStatus,
@ -14,7 +14,7 @@ import {
* @class ContinueAction
* @implements {Action}
*/
export class ContinueAction implements Action {
export class ContinueAction implements RendererAction {
async run(
action: ListenerAction,
renderer: ListenerContext,

View File

@ -1,7 +1,7 @@
import {RendererEvent} from '../utils/renderer-event';
import {filter} from '../utils/tpl';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
LoopStatus,
@ -20,7 +20,7 @@ export interface ICopyAction extends ListenerAction {
* @class CopyAction
* @implements {Action}
*/
export class CopyAction implements Action {
export class CopyAction implements RendererAction {
async run(
action: ICopyAction,
renderer: ListenerContext,

View File

@ -1,6 +1,6 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
LoopStatus,
@ -18,7 +18,7 @@ export interface ICustomAction extends ListenerAction {
* @class CustomAction
* @implements {Action}
*/
export class CustomAction implements Action {
export class CustomAction implements RendererAction {
async run(
action: ICustomAction,
renderer: ListenerContext,

View File

@ -1,7 +1,7 @@
import {SchemaNode} from '../types';
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
registerAction
@ -27,7 +27,7 @@ export interface IDialogAction extends ListenerAction {
* @class DialogAction
* @implements {Action}
*/
export class DialogAction implements Action {
export class DialogAction implements RendererAction {
async run(
action: IDialogAction,
renderer: ListenerContext,
@ -44,7 +44,7 @@ export class DialogAction implements Action {
* @class CloseDialogAction
* @implements {Action}
*/
export class CloseDialogAction implements Action {
export class CloseDialogAction implements RendererAction {
async run(
action: ListenerAction,
renderer: ListenerContext,
@ -70,7 +70,7 @@ export class CloseDialogAction implements Action {
/**
* alert提示动作
*/
export class AlertAction implements Action {
export class AlertAction implements RendererAction {
async run(
action: IAlertAction,
renderer: ListenerContext,
@ -83,7 +83,7 @@ export class AlertAction implements Action {
/**
* confirm确认提示动作
*/
export class ConfirmAction implements Action {
export class ConfirmAction implements RendererAction {
async run(
action: IConfirmAction,
renderer: ListenerContext,

View File

@ -1,7 +1,7 @@
import {SchemaNode} from '../types';
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
registerAction
@ -18,7 +18,7 @@ export interface IDrawerAction extends ListenerAction {
* @class DrawerAction
* @implements {Action}
*/
export class DrawerAction implements Action {
export class DrawerAction implements RendererAction {
async run(
action: IDrawerAction,
renderer: ListenerContext,
@ -35,7 +35,7 @@ export class DrawerAction implements Action {
* @class CloseDrawerAction
* @implements {Action}
*/
export class CloseDrawerAction implements Action {
export class CloseDrawerAction implements RendererAction {
async run(
action: ListenerAction,
renderer: ListenerContext,

View File

@ -4,7 +4,7 @@ import pick from 'lodash/pick';
import mapValues from 'lodash/mapValues';
import qs from 'qs';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
registerAction
@ -25,7 +25,7 @@ export interface IEmailAction extends ListenerAction {
* @class EmailAction
* @implements {Action}
*/
export class EmailAction implements Action {
export class EmailAction implements RendererAction {
async run(
action: IEmailAction,
renderer: ListenerContext,

View File

@ -1,9 +1,10 @@
import {Action} from '../types';
import {buildApi} from '../utils/api';
import {isEmpty, isObject, qsstringify} from '../utils/helper';
import {RendererEvent} from '../utils/renderer-event';
import {filter} from '../utils/tpl';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
registerAction
@ -12,6 +13,7 @@ import {
export interface ILinkAction extends ListenerAction {
link: string;
url?: never;
blank?: boolean;
params?: {
[key: string]: string;
};
@ -20,6 +22,7 @@ export interface ILinkAction extends ListenerAction {
export interface IUrlAction extends ListenerAction {
url: string;
link?: never;
blank?: boolean;
params?: {
[key: string]: string;
};
@ -32,7 +35,7 @@ export interface IUrlAction extends ListenerAction {
* @class LinkAction
* @implements {Action}
*/
export class LinkAction implements Action {
export class LinkAction implements RendererAction {
async run(
action: ILinkAction | IUrlAction,
renderer: ListenerContext,
@ -54,7 +57,7 @@ export class LinkAction implements Action {
}
);
renderer.props.env.jumpTo(urlObj.url, action, action.args);
renderer.props.env.jumpTo(urlObj.url, action as Action, action.args);
}
}

View File

@ -1,7 +1,7 @@
import {RendererEvent} from '../utils/renderer-event';
import {createObject} from '../utils/helper';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
LogicAction,
@ -23,7 +23,7 @@ export interface ILoopAction extends ListenerAction, LogicAction {
* @class LoopAction
* @implements {Action}
*/
export class LoopAction implements Action {
export class LoopAction implements RendererAction {
async run(
action: ILoopAction,
renderer: ListenerContext,

View File

@ -1,6 +1,6 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
registerAction
@ -17,7 +17,7 @@ export interface IPageGoAction extends ListenerAction {
* @class PageGoBackAction
* @implements {Action}
*/
export class PageGoBackAction implements Action {
export class PageGoBackAction implements RendererAction {
async run(
action: ListenerAction,
renderer: ListenerContext,
@ -34,7 +34,7 @@ export class PageGoBackAction implements Action {
* @class PageGoAction
* @implements {Action}
*/
export class PageGoAction implements Action {
export class PageGoAction implements RendererAction {
async run(
action: IPageGoAction,
renderer: ListenerContext,
@ -51,7 +51,7 @@ export class PageGoAction implements Action {
* @class PageRefreshAction
* @implements {Action}
*/
export class PageRefreshAction implements Action {
export class PageRefreshAction implements RendererAction {
async run(
action: ListenerAction,
renderer: ListenerContext,

View File

@ -1,6 +1,6 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
LogicAction,
@ -10,7 +10,7 @@ import {
export interface IParallelAction extends ListenerAction, LogicAction {}
export class ParallelAction implements Action {
export class ParallelAction implements RendererAction {
async run(
action: IParallelAction,
renderer: ListenerContext,

View File

@ -2,7 +2,7 @@ import {inflate} from 'zlib';
import {RendererEvent} from '../utils/renderer-event';
import {evalExpression} from '../utils/tpl';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
LogicAction,
@ -15,7 +15,7 @@ export interface ISwitchAction extends ListenerAction, LogicAction {}
/**
*
*/
export class SwitchAction implements Action {
export class SwitchAction implements RendererAction {
async run(
action: ISwitchAction,
renderer: ListenerContext,

View File

@ -1,6 +1,6 @@
import {RendererEvent} from '../utils/renderer-event';
import {
Action,
RendererAction,
ListenerAction,
ListenerContext,
registerAction
@ -26,7 +26,7 @@ export interface IToastAction extends ListenerAction {
/**
* toast
*/
export class ToastAction implements Action {
export class ToastAction implements RendererAction {
async run(
action: IToastAction,
renderer: ListenerContext,

View File

@ -6,12 +6,6 @@ import {RendererConfig} from './factory';
import {ThemeInstance} from './theme';
import {Action, Api, Payload, Schema} from './types';
import hoistNonReactStatic from 'hoist-non-react-statics';
import {
RendererEvent,
RendererEventListener,
EventListeners
} from './utils/renderer-event';
import {IScopedContext} from './Scoped';
export interface RendererEnv {
fetcher: (api: Api, data?: any, options?: object) => Promise<Payload>;
@ -69,26 +63,6 @@ export interface RendererEnv {
) => Promise<React.ReactType> | React.ReactType | JSX.Element | void;
loadChartExtends?: () => void | Promise<void>;
useMobileUI?: boolean;
bindEvent: (context: any) => (() => void) | undefined;
dispatchEvent: (
e:
| string
| React.ClipboardEvent<any>
| React.DragEvent<any>
| React.ChangeEvent<any>
| React.KeyboardEvent<any>
| React.TouchEvent<any>
| React.WheelEvent<any>
| React.AnimationEvent<any>
| React.TransitionEvent<any>
| React.MouseEvent<any>,
context: any,
scoped: IScopedContext,
data: any,
broadcast?: RendererEvent<any>
) => Promise<RendererEvent<any> | undefined>;
rendererEventListeners: RendererEventListener[];
/**
* html xss
*/

View File

@ -4,7 +4,6 @@ import {getEnv, destroy} from 'mobx-state-tree';
import {wrapFetcher} from './utils/api';
import {normalizeLink} from './utils/normalizeLink';
import {
createObject,
findIndex,
isObject,
JSONTraverse,
@ -13,9 +12,7 @@ import {
string2regExp
} from './utils/helper';
import {
Api,
fetcherResult,
Payload,
SchemaNode,
Schema,
Action,
@ -23,8 +20,8 @@ import {
PlainObject
} from './types';
import {observer} from 'mobx-react';
import Scoped, {IScopedContext} from './Scoped';
import {getTheme, ThemeInstance, ThemeProps} from './theme';
import Scoped from './Scoped';
import {getTheme, ThemeProps} from './theme';
import find from 'lodash/find';
import Alert from './components/Alert2';
import {toast} from './components/Toast';
@ -34,14 +31,7 @@ import ScopedRootRenderer, {RootRenderProps} from './Root';
import {HocStoreFactory} from './WithStore';
import {EnvContext, RendererEnv} from './env';
import {envOverwrite} from './envOverwrite';
import {
EventListeners,
createRendererEvent,
RendererEventListener,
OnEventProps,
RendererEvent
} from './utils/renderer-event';
import {runActions} from './actions/Action';
import {OnEventProps} from './utils/renderer-event';
import {enableDebug} from './utils/debug';
export interface TestFunc {
@ -283,7 +273,6 @@ const defaultOptions: RenderOptions = {
location.search.indexOf('amisDebug=1') !== -1 ??
false,
loadRenderer,
rendererEventListeners: [],
fetcher() {
return Promise.reject('fetcher is required');
},
@ -384,98 +373,6 @@ const defaultOptions: RenderOptions = {
},
// 用于跟踪用户在界面中的各种操作
tracker(eventTrack: EventTrack, props: PlainObject) {},
// 返回解绑函数
bindEvent(renderer: any) {
if (!renderer) {
return undefined;
}
const listeners: EventListeners = renderer.props.$schema.onEvent;
if (listeners) {
// 暂存
for (let key of Object.keys(listeners)) {
const listener = this.rendererEventListeners.some(
(item: RendererEventListener) =>
item.renderer === renderer && item.type === key
);
if (!listener) {
this.rendererEventListeners.push({
renderer,
type: key,
weight: listeners[key].weight || 0,
actions: listeners[key].actions
});
}
}
return () => {
this.rendererEventListeners = this.rendererEventListeners.filter(
(item: RendererEventListener) => item.renderer !== renderer
);
};
}
return undefined;
},
async dispatchEvent(
e: string | React.MouseEvent<any>,
renderer: React.Component<RendererProps>,
scoped: IScopedContext,
data: any,
broadcast?: RendererEvent<any>
) {
let unbindEvent = null;
const eventName = typeof e === 'string' ? e : e.type;
if (!broadcast) {
const eventConfig = renderer?.props?.onEvent?.[eventName];
if (!eventConfig) {
// 没命中也没关系
return Promise.resolve(undefined);
}
unbindEvent = this.bindEvent(renderer);
}
// 没有可处理的监听
if (!this.rendererEventListeners.length) {
return Promise.resolve();
}
// 如果是广播动作,就直接复用
const rendererEvent =
broadcast ||
createRendererEvent(eventName, {
env: this,
nativeEvent: e,
data,
scoped
});
// 过滤&排序
const listeners = this.rendererEventListeners
.filter(
(item: RendererEventListener) =>
item.type === eventName &&
(broadcast ? true : item.renderer === renderer)
)
.sort(
(prev: RendererEventListener, next: RendererEventListener) =>
next.weight - prev.weight
);
for (let listener of listeners) {
await runActions(listener.actions, listener.renderer, rendererEvent);
// 停止后续监听器执行
if (rendererEvent.stoped) {
break;
}
}
unbindEvent?.();
return rendererEvent;
},
rendererResolver: resolveRenderer,
replaceTextIgnoreKeys: [
'type',

View File

@ -1,4 +1,6 @@
import {ListenerAction, ListenerContext} from '../actions/Action';
import {ListenerAction, ListenerContext, runActions} from '../actions/Action';
import {RendererProps} from '../factory';
import {IScopedContext} from '../Scoped';
// 事件监听器
export interface EventListeners {
@ -20,7 +22,7 @@ export interface OnEventProps {
// 渲染器事件监听器
export interface RendererEventListener {
renderer: ListenerContext;
renderer: React.Component<RendererProps>;
type: string;
weight: number;
actions: ListenerAction[];
@ -43,6 +45,8 @@ export interface RendererEventContext {
[propName: string]: any;
}
let rendererEventListeners: RendererEventListener[] = [];
// 创建渲染器事件对象
export function createRendererEvent<T extends RendererEventContext>(
type: string,
@ -72,4 +76,103 @@ export function createRendererEvent<T extends RendererEventContext>(
return rendererEvent;
}
// 绑定事件
export const bindEvent = (renderer: any) => {
if (!renderer) {
return undefined;
}
const listeners: EventListeners = renderer.props.$schema.onEvent;
if (listeners) {
// 暂存
for (let key of Object.keys(listeners)) {
const listener = rendererEventListeners.some(
(item: RendererEventListener) =>
item.renderer === renderer && item.type === key
);
if (!listener) {
rendererEventListeners.push({
renderer,
type: key,
weight: listeners[key].weight || 0,
actions: listeners[key].actions
});
}
}
return () => {
rendererEventListeners = rendererEventListeners.filter(
(item: RendererEventListener) => item.renderer !== renderer
);
};
}
return undefined;
};
// 触发事件
export async function dispatchEvent(
e: string | React.MouseEvent<any>,
renderer: React.Component<RendererProps>,
scoped: IScopedContext,
data: any,
broadcast?: RendererEvent<any>
): Promise<RendererEvent<any> | void> {
let unbindEvent = null;
const eventName = typeof e === 'string' ? e : e.type;
if (!broadcast) {
const eventConfig = renderer?.props?.onEvent?.[eventName];
if (!eventConfig) {
// 没命中也没关系
return Promise.resolve();
}
unbindEvent = bindEvent(renderer);
}
// 没有可处理的监听
if (!rendererEventListeners.length) {
return Promise.resolve();
}
// 如果是广播动作,就直接复用
const rendererEvent =
broadcast ||
createRendererEvent(eventName, {
env: renderer?.props?.env,
nativeEvent: e,
data,
scoped
});
// 过滤&排序
const listeners = rendererEventListeners
.filter(
(item: RendererEventListener) =>
item.type === eventName &&
(broadcast ? true : item.renderer === renderer)
)
.sort(
(prev: RendererEventListener, next: RendererEventListener) =>
next.weight - prev.weight
);
for (let listener of listeners) {
await runActions(listener.actions, listener.renderer, rendererEvent);
// 停止后续监听器执行
if (rendererEvent.stoped) {
break;
}
}
unbindEvent?.();
return Promise.resolve(rendererEvent);
}
export const getRendererEventListeners = () => {
return rendererEventListeners;
};
export default {};