This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
import React from 'react';
import type { PortalMethods } from './context';
export type PortalConsumerProps = {
hostName: string;
manager: PortalMethods;
children: React.ReactNode;
};
export class PortalConsumer extends React.Component<PortalConsumerProps> {
_key: any;
componentDidMount() {
if (!this.props.manager) {
throw new Error(
'Looks like you forgot to wrap your root component with `PortalHost` component.\n'
);
}
this._key = this.props.manager.mount(
this.props.hostName,
this.props.children
);
}
componentDidUpdate() {
this.props.manager.update(
this.props.hostName,
this._key,
this.props.children
);
}
componentWillUnmount() {
this.props.manager.unmount(this.props.hostName, this._key);
}
render() {
return null;
}
}

View File

@@ -0,0 +1,64 @@
import React from 'react';
export type State = {
portals: {
key: number;
children: React.ReactNode;
}[];
};
interface PortalManagerProps {
renderManagerView: (children: React.ReactNode) => React.ReactElement;
}
export interface PortalManagerState {
portals: any[];
}
/**
* Portal host is the component which actually renders all Portals.
*/
export class PortalManager extends React.PureComponent<
PortalManagerProps,
PortalManagerState
> {
state: State = {
portals: [],
};
mount = (key: number, children: React.ReactNode) => {
this.setState((state) => ({
portals: [...state.portals, { key, children }],
}));
};
update = (key: number, children: React.ReactNode) => {
this.setState((state) => ({
portals: state.portals.map((item) => {
if (item.key === key) {
return { ...item, children };
}
return item;
}),
}));
};
unmount = (key: number) => {
this.setState((state) => ({
portals: state.portals.filter((item) => item.key !== key),
}));
};
render() {
const { renderManagerView } = this.props;
return this.state.portals.map(({ key, children }, i) => (
<React.Fragment key={key}>{renderManagerView(children)}</React.Fragment>
// <View
// key={key}
// collapsable={
// false /* Need collapsable=false here to clip the elevations, otherwise they appear above sibling components */
// }
// pointerEvents="box-none"
// style={[StyleSheet.absoluteFill, { zIndex: 1000 + i }]}
// >
// {children}
// </View>
));
}
}

View File

@@ -0,0 +1,10 @@
## Portal
**暂时没有使用**
这是一组用于使用命令式方法将一个React DOM映射到相关的位置的方法
可以指定不同的portal位置
用于处理对于每个节点都需要创建一个相关的Modal的情况
> 参考 https://github.com/ant-design/ant-design-mobile-rn/blob/master/components/portal/ 的实现

View 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 };
}

View File

@@ -0,0 +1,14 @@
import React from 'react';
export type PortalMethods = {
mount: (name: string, children: React.ReactNode) => number;
update: (name: string, key: number, children: React.ReactNode) => void;
unmount: (name: string, key: number) => void;
};
export function createPortalContext(name: string) {
const PortalContext = React.createContext<PortalMethods | null>(null);
PortalContext.displayName = 'PortalContext-' + name;
return PortalContext;
}

View File

@@ -0,0 +1,40 @@
export class DefaultEventEmitter {
// 参考: https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget
listeners: any = {};
emit(type: string, ...args: any[]) {
if (!(type in this.listeners)) {
return;
}
const stack = this.listeners[type];
for (let i = 0, l = stack.length; i < l; i++) {
stack[i].call(this, event);
const func = stack[i];
if (typeof func === 'function') {
func(...args);
}
}
}
addListener(type: string, callback: (...args: any[]) => any) {
if (!(type in this.listeners)) {
this.listeners[type] = [];
}
this.listeners[type].push(callback);
}
removeListener(type: string, callback: (...args: any[]) => any): any {
if (!(type in this.listeners)) {
return;
}
const stack = this.listeners[type];
for (let i = 0, l = stack.length; i < l; i++) {
if (stack[i] === callback) {
stack.splice(i, 1);
return this.removeListener(type, callback);
}
}
}
}

View File

@@ -0,0 +1,2 @@
export { buildPortal } from './buildPortal';
export { DefaultEventEmitter } from './defaultEventEmitter';