优化
This commit is contained in:
206
client/shared/components/Portal/buildPortal.tsx
Normal file
206
client/shared/components/Portal/buildPortal.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useRef,
|
||||
Fragment,
|
||||
useContext,
|
||||
PropsWithChildren,
|
||||
} from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { PortalManager } from './Manager';
|
||||
import { createPortalContext } from './context';
|
||||
import { PortalConsumer } from './Consumer';
|
||||
import _isNil from 'lodash/isNil';
|
||||
import { DefaultEventEmitter } from './defaultEventEmitter';
|
||||
|
||||
type Operation =
|
||||
| { type: 'mount'; key: number; children: React.ReactNode }
|
||||
| { type: 'update'; key: number; children: React.ReactNode }
|
||||
| { type: 'unmount'; key: number };
|
||||
|
||||
// Events const
|
||||
const addType = 'ADD_PORTAL';
|
||||
const removeType = 'REMOVE_PORTAL';
|
||||
|
||||
// For react-native
|
||||
// const TopViewEventEmitter = DeviceEventEmitter || new NativeEventEmitter();
|
||||
|
||||
const defaultRenderManagerViewFn = (children: React.ReactNode) => (
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
interface EventEmitterFunc {
|
||||
emit: (...args: any[]) => any;
|
||||
addListener: (...args: any[]) => any;
|
||||
removeListener: (...args: any[]) => any;
|
||||
}
|
||||
export interface BuildPortalOptions {
|
||||
/**
|
||||
* 唯一标识名
|
||||
* 用于多实例的情况
|
||||
*/
|
||||
hostName?: string;
|
||||
|
||||
/**
|
||||
* 事件监听函数
|
||||
*/
|
||||
eventEmitter?: EventEmitterFunc;
|
||||
|
||||
/**
|
||||
* 负责Portal Manager如何生成函数的逻辑
|
||||
*/
|
||||
renderManagerView?: (children: React.ReactNode) => React.ReactElement;
|
||||
}
|
||||
export function buildPortal(options: BuildPortalOptions) {
|
||||
const {
|
||||
hostName = 'default',
|
||||
eventEmitter = new DefaultEventEmitter(),
|
||||
renderManagerView = defaultRenderManagerViewFn,
|
||||
} = options;
|
||||
let nextKey = 10000;
|
||||
|
||||
const add = (el: React.ReactNode): number => {
|
||||
const key = nextKey++;
|
||||
eventEmitter.emit(addType, hostName, el, key);
|
||||
return key;
|
||||
};
|
||||
|
||||
const remove = (key: number): void => {
|
||||
eventEmitter.emit(removeType, hostName, key);
|
||||
};
|
||||
|
||||
const PortalContext = createPortalContext(hostName);
|
||||
|
||||
const PortalHost: React.FC<PropsWithChildren> = React.memo((props) => {
|
||||
const managerRef = useRef<PortalManager>();
|
||||
const nextKeyRef = useRef<number>(0);
|
||||
const queueRef = useRef<Operation[]>([]);
|
||||
const hostNameRef = useRef(hostName);
|
||||
useEffect(() => {
|
||||
hostNameRef.current = hostName;
|
||||
}, [hostName]);
|
||||
|
||||
const mount: any = useCallback(
|
||||
(name: string, children: React.ReactNode, _key?: number) => {
|
||||
if (name !== hostNameRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = _key || nextKeyRef.current++;
|
||||
if (managerRef.current) {
|
||||
managerRef.current.mount(key, children);
|
||||
} else {
|
||||
queueRef.current.push({ type: 'mount', key, children });
|
||||
}
|
||||
|
||||
return key;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
(name: string, key: number, children: React.ReactNode) => {
|
||||
if (name !== hostNameRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (managerRef.current) {
|
||||
managerRef.current.update(key, children);
|
||||
} else {
|
||||
const op: Operation = { type: 'mount', key, children };
|
||||
const index = queueRef.current.findIndex(
|
||||
(o) => o.type === 'mount' || (o.type === 'update' && o.key === key)
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
queueRef.current[index] = op;
|
||||
} else {
|
||||
queueRef.current.push(op);
|
||||
}
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const unmount = useCallback((name: string, key: number) => {
|
||||
if (name !== hostNameRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (managerRef.current) {
|
||||
managerRef.current.unmount(key);
|
||||
} else {
|
||||
queueRef.current.push({ type: 'unmount', key });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
eventEmitter.addListener(addType, mount);
|
||||
eventEmitter.addListener(removeType, unmount);
|
||||
|
||||
return () => {
|
||||
eventEmitter.removeListener(addType, mount);
|
||||
eventEmitter.removeListener(removeType, unmount);
|
||||
};
|
||||
}, [mount, unmount]);
|
||||
|
||||
useEffect(() => {
|
||||
// 处理队列
|
||||
const queue = queueRef.current;
|
||||
const manager = managerRef.current;
|
||||
|
||||
while (queue.length && manager) {
|
||||
const action = queue.pop();
|
||||
if (!action) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case 'mount':
|
||||
manager.mount(action.key, action.children);
|
||||
break;
|
||||
case 'update':
|
||||
manager.update(action.key, action.children);
|
||||
break;
|
||||
case 'unmount':
|
||||
manager.unmount(action.key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PortalContext.Provider
|
||||
value={{
|
||||
mount,
|
||||
update,
|
||||
unmount,
|
||||
}}
|
||||
>
|
||||
<Fragment>{props.children}</Fragment>
|
||||
<PortalManager
|
||||
ref={managerRef as any}
|
||||
renderManagerView={renderManagerView}
|
||||
/>
|
||||
</PortalContext.Provider>
|
||||
);
|
||||
});
|
||||
PortalHost.displayName = 'PortalHost-' + hostName;
|
||||
|
||||
const PortalRender: React.FC<PropsWithChildren> = React.memo((props) => {
|
||||
const manager = useContext(PortalContext);
|
||||
|
||||
if (_isNil(manager)) {
|
||||
console.error('Not find PortalContext');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalConsumer hostName={hostName} manager={manager}>
|
||||
{props.children}
|
||||
</PortalConsumer>
|
||||
);
|
||||
});
|
||||
PortalRender.displayName = 'PortalRender-' + hostName;
|
||||
|
||||
return { add, remove, PortalHost, PortalRender };
|
||||
}
|
||||
Reference in New Issue
Block a user