diff --git a/packages/inula/__tests__/HorizonXTest/adapters/ReduxReactAdapter.test.tsx b/packages/inula/__tests__/HorizonXTest/adapters/ReduxReactAdapter.test.tsx
index b03ea1f5..ea9b58e0 100644
--- a/packages/inula/__tests__/HorizonXTest/adapters/ReduxReactAdapter.test.tsx
+++ b/packages/inula/__tests__/HorizonXTest/adapters/ReduxReactAdapter.test.tsx
@@ -375,4 +375,93 @@ describe('Redux/React binding adapter', () => {
expect(getE(BUTTON).innerHTML).toBe('1');
expect(getE(BUTTON2).innerHTML).toBe('true');
});
+
+ it('Nested use of connect', () => {
+ const updateInfo = [];
+ let dispatchMethod;
+ const ChildComponent = ({ childData, dispatch }) => {
+ const isMount = Inula.useRef(false);
+
+ Inula.useEffect(() => {
+ if (!isMount.current) {
+ isMount.current = true;
+ } else {
+ updateInfo.push('ChildComponent Updated');
+ }
+ });
+ dispatchMethod = dispatch;
+
+ return (
+
+
Child Component
+
{childData}
+
+ );
+ };
+
+ const mapStateToPropsChild = state => ({ childData: state.childData });
+
+ const Child = connect(mapStateToPropsChild)(ChildComponent);
+
+ // Parent Component
+ const ParentComponent = ({ parentData }) => {
+ const isMount = Inula.useRef(false);
+
+ Inula.useEffect(() => {
+ if (!isMount.current) {
+ isMount.current = true;
+ } else {
+ updateInfo.push('ParentComponent Updated');
+ }
+ });
+
+ return (
+
+
Parent Component
+
{parentData}
+
+
+ );
+ };
+ const mapStateToPropsParent = state => ({
+ parentData: state.parentData,
+ });
+
+ const Parent = connect(mapStateToPropsParent)(ParentComponent);
+
+ const initialState = {
+ parentData: 0,
+ childData: 0,
+ };
+
+ const reducer = (state = initialState, action) => {
+ switch (action.type) {
+ case 'INCREMENT_PARENT':
+ return { ...state, parentData: state.parentData + 1 };
+ case 'INCREMENT_CHILD':
+ return { ...state, childData: state.childData + 1 };
+ default:
+ return state;
+ }
+ };
+
+ const store = createStore(reducer);
+
+ Inula.render(
+
+
+ ,
+ getE(CONTAINER)
+ );
+ expect(getE('child').innerHTML).toBe('0');
+ expect(getE('parent').innerHTML).toBe('0');
+
+ Inula.act(() => {
+ dispatchMethod({ type: 'INCREMENT_CHILD' });
+ });
+ expect(updateInfo).toStrictEqual(['ChildComponent Updated']);
+
+ expect(getE('child').innerHTML).toBe('1');
+ expect(getE('parent').innerHTML).toBe('0');
+ });
});
diff --git a/packages/inula/src/inulax/adapters/redux.ts b/packages/inula/src/inulax/adapters/redux.ts
index 99e57748..a57219a1 100644
--- a/packages/inula/src/inulax/adapters/redux.ts
+++ b/packages/inula/src/inulax/adapters/redux.ts
@@ -184,7 +184,7 @@ type ActionCreator = (...params: any[]) => ReduxAction;
type ActionCreators = { [key: string]: ActionCreator };
export type BoundActionCreator = (...params: any[]) => void;
type BoundActionCreators = { [key: string]: BoundActionCreator };
-type Dispatch = (action: ReduxAction) => any;
+export type Dispatch = (action: ReduxAction) => any;
export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators {
const boundActionCreators = {};
diff --git a/packages/inula/src/inulax/adapters/reduxReact.ts b/packages/inula/src/inulax/adapters/reduxReact.ts
index 4209492d..15badad2 100644
--- a/packages/inula/src/inulax/adapters/reduxReact.ts
+++ b/packages/inula/src/inulax/adapters/reduxReact.ts
@@ -13,14 +13,28 @@
* See the Mulan PSL v2 for more details.
*/
-import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/HookExternal';
+import { useContext, useEffect, useLayoutEffect, useMemo, useReducer, useRef } from '../../renderer/hooks/HookExternal';
import { createContext } from '../../renderer/components/context/CreateContext';
import { createElement } from '../../external/JSXElement';
-import type { ReduxStoreHandler, ReduxAction, BoundActionCreator } from './redux';
+import type { BoundActionCreator, ReduxAction, ReduxStoreHandler } from './redux';
import { forwardRef } from '../../renderer/components/ForwardRef';
+import createSubscription, { Subscription } from './subscription';
+import { ForwardRef } from '../../types';
+import { isContextConsumer } from '../../external/InulaIs';
+import { getSelector } from './reduxSelector';
-const DefaultContext = createContext(null);
+const DefaultContext = createContext<{ store: ReduxStoreHandler; subscription: Subscription }>(null as any);
type Context = typeof DefaultContext;
+type Selector = (state: unknown) => unknown;
+
+type ConnectProps = {
+ store: ReduxStoreHandler;
+ context?: Context;
+};
+
+type WrapperInnerProps = {
+ reduxAdapterRef?: ForwardRef;
+};
export function Provider({
store,
@@ -28,32 +42,44 @@ export function Provider({
children,
}: {
store: ReduxStoreHandler;
- context: Context;
+ context?: Context;
children?: any[];
}) {
+ const ctxValue = useMemo(() => {
+ const subscription = createSubscription(store);
+ return {
+ store,
+ subscription,
+ };
+ }, [store]);
+ const prevStoreValue = useMemo(() => store.getState(), [store]);
+ useLayoutEffect(() => {
+ const subscription = ctxValue.subscription;
+ subscription.stateChange = subscription.triggerNestedSubs;
+ subscription.trySubscribe();
+ if (prevStoreValue !== store.getState()) {
+ subscription.triggerNestedSubs();
+ }
+ return () => {
+ subscription.trySubscribe();
+ subscription.stateChange = undefined;
+ };
+ }, [ctxValue, prevStoreValue]);
+
const Context = context; // NOTE: bind redux API to inula API requires this renaming;
- return createElement(Context.Provider, { value: store }, children);
+ return createElement(Context.Provider, { value: ctxValue }, children);
}
export function createStoreHook(context: Context): () => ReduxStoreHandler {
return () => {
- return useContext(context) as unknown as ReduxStoreHandler;
+ return useContext(context).store;
};
}
-export function createSelectorHook(context: Context): (selector?: ((state: unknown) => any) | undefined) => any {
+export function createSelectorHook(context: Context) {
const store = createStoreHook(context)();
- return function useSelector(selector = state => state) {
- const [state, setState] = useState(() => store.getState());
-
- useEffect(() => {
- const unsubscribe = store.subscribe(() => {
- setState(store.getState());
- });
- return () => unsubscribe();
- }, []);
-
- return selector(state);
+ return function useSelector(selector: Selector = state => state) {
+ return useSelectorWithStore(store, selector);
};
}
@@ -64,7 +90,7 @@ export function createDispatchHook(context: Context): () => BoundActionCreator {
};
}
-export const useSelector = selector => {
+export const useSelector = (selector: Selector) => {
return createSelectorHook(DefaultContext)(selector);
};
@@ -76,11 +102,11 @@ export const useStore = () => {
return createStoreHook(DefaultContext)();
};
-type MapStateToPropsP = (state: any, ownProps: OwnProps) => StateProps;
-type MapDispatchToPropsP =
+export type MapStateToPropsP = (state: any, ownProps: OwnProps) => StateProps;
+export type MapDispatchToPropsP =
| { [key: string]: (...args: any[]) => ReduxAction }
| ((dispatch: (action: ReduxAction) => any, ownProps: OwnProps) => DispatchProps);
-type MergePropsP = (
+export type MergePropsP = (
stateProps: StateProps,
dispatchProps: DispatchProps,
ownProps: OwnProps
@@ -89,79 +115,131 @@ type MergePropsP = (
type WrappedComponent = (props: OwnProps) => ReturnType;
type OriginalComponent = (props: MergedProps) => ReturnType;
type Connector = (Component: OriginalComponent) => WrappedComponent;
-type ConnectOption = {
- areStatesEqual?: (oldState: State, newState: State) => boolean;
- context?: Context;
+export type ConnectOption = {
+ /** @deprecated */
+ prue?: boolean;
forwardRef?: boolean;
+ context?: Context;
+ areOwnPropsEqual?: (newOwnProps: OwnProps, oldOwnProps: OwnProps) => boolean;
+ areStatePropsEqual?: (newStateProps: StateProps, oldStateProps: StateProps) => boolean;
+ areStatesEqual?: (oldState: State, newState: State) => boolean;
};
export function connect(
mapStateToProps: MapStateToPropsP = () => ({}) as StateProps,
- mapDispatchToProps: MapDispatchToPropsP = () => ({}) as DispatchProps,
- mergeProps: MergePropsP = (
- stateProps,
- dispatchProps,
- ownProps
- ): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps }) as unknown as MergedProps,
- options: ConnectOption = {}
+ mapDispatchToProps?: MapDispatchToPropsP,
+ mergeProps?: MergePropsP,
+ options: ConnectOption = {}
): Connector {
//this component should bear the type returned from mapping functions
- return (Component: OriginalComponent): WrappedComponent => {
- const useStore = createStoreHook(options.context || DefaultContext);
+ const selectorOptions = {
+ mapStateToProps,
+ mapDispatchToProps,
+ mergeProps,
+ options,
+ };
+
+ const { context: storeContext = DefaultContext } = options;
+
+ return (Component: OriginalComponent): WrappedComponent => {
//this component should mimic original type of component used
- const Wrapper: WrappedComponent = (props: OwnProps) => {
- const store = useStore() as ReduxStoreHandler;
- const [state, setState] = useState(() => store.getState());
+ const Wrapper: WrappedComponent = (props: OwnProps & ConnectProps & WrapperInnerProps) => {
+ const [, forceUpdate] = useReducer(s => s + 1, 0);
+
+ const propsFromContext = props.context;
+ const { reduxAdapterRef, ...wrappedProps } = props;
+ const usedContext = useMemo(() => {
+ return propsFromContext &&
+ propsFromContext.Consumer &&
+ isContextConsumer(createElement(propsFromContext.Consumer, {}))
+ ? propsFromContext
+ : storeContext;
+ }, [propsFromContext, storeContext]);
+
+ const context = useContext(usedContext);
+ // 判断store是来自context还是props
+ const isStoreFromProps = !!props.store && !!props.store.getState && !!props.store.dispatch;
+
+ const store = isStoreFromProps ? props.store : context.store;
+
+ const [subscription, triggerNestedSubs] = useMemo(() => {
+ const subscription = createSubscription(store, isStoreFromProps ? null : context.subscription);
+ const triggerNestedSubs = subscription.triggerNestedSubs.bind(subscription);
+ return [subscription, triggerNestedSubs];
+ }, [store, isStoreFromProps, context]);
+
+ /**
+ * 如果在调用listener中间组件被卸载,subscription会变为空
+ * 在一开始就复制一份triggerNestedSubs保证即使组件卸载也可以正常使用
+ */
+
+ const overrideContext = useMemo(
+ () => (isStoreFromProps ? context : { ...context, subscription }),
+ [isStoreFromProps, context, subscription]
+ );
+
+ // 使用Ref存储最新的子组件Props,在更新时进行比较,防止重复渲染
+ const latestChildProps = useRef();
+ const latestWrappedProps = useRef(wrappedProps);
+ const childPropsFormStore = useRef();
+ const isRendering = useRef(false);
+
+ const selector = useMemo(() => getSelector(store, selectorOptions), [store]);
+
+ const childProps = useMemo(() => {
+ return childPropsFormStore.current && wrappedProps === latestChildProps.current
+ ? childPropsFormStore.current
+ : selector(store.getState(), wrappedProps as OwnProps);
+ }, [store, wrappedProps, latestWrappedProps]);
useEffect(() => {
- const unsubscribe = store.subscribe(() => {
- setState(store.getState());
- });
- return () => unsubscribe();
- }, []);
-
- const previous = useRef<{ state: { [key: string]: any }; mappedState: StateProps }>({
- state: {},
- mappedState: {} as StateProps,
+ latestChildProps.current = childProps;
+ latestWrappedProps.current = wrappedProps;
+ isRendering.current = false;
+ if (childPropsFormStore.current) {
+ childPropsFormStore.current = null;
+ triggerNestedSubs();
+ }
});
- let mappedState: StateProps;
- if (options.areStatesEqual) {
- if (options.areStatesEqual(previous.current.state, state)) {
- mappedState = previous.current.mappedState as StateProps;
- } else {
- mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps);
- previous.current.mappedState = mappedState;
- }
- } else {
- mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps);
- previous.current.mappedState = mappedState;
- }
- let mappedDispatch: DispatchProps = {} as DispatchProps;
- if (mapDispatchToProps) {
- if (typeof mapDispatchToProps === 'object') {
- Object.entries(mapDispatchToProps).forEach(([key, value]) => {
- mappedDispatch[key] = (...args: ReduxAction[]) => {
- store.dispatch(value(...args));
- setState(store.getState());
- };
- });
- } else {
- mappedDispatch = mapDispatchToProps(store.dispatch, props);
- }
- }
- mappedDispatch = Object.assign({}, mappedDispatch, { dispatch: store.dispatch });
- const mergedProps = (
- mergeProps ||
- ((state, dispatch, originalProps) => {
- return { ...state, ...dispatch, ...originalProps };
- })
- )(mappedState, mappedDispatch, props);
+ useEffect(() => {
+ let isUnsubscribe = false;
- previous.current.state = state;
+ const update = () => {
+ if (isUnsubscribe) {
+ return;
+ }
+ const latestStoreState = store.getState();
+ const newChildProps = selector(latestStoreState, latestWrappedProps.current as OwnProps);
+ // 如果新的子组件的props和之前不同,就更新ref对象的值,并强制更新组件
+ if (newChildProps === latestChildProps.current) {
+ if (!isRendering.current) {
+ triggerNestedSubs();
+ }
+ } else {
+ latestChildProps.current = newChildProps;
+ childPropsFormStore.current = newChildProps;
+ isRendering.current = true;
+ forceUpdate();
+ }
+ };
+ // 订阅store的变化
+ subscription.stateChange = update;
+ subscription.trySubscribe();
+ update();
+ return () => {
+ isUnsubscribe = true;
+ subscription.trySubscribe();
+ subscription.stateChange = undefined;
+ };
+ }, [store, subscription, selector]);
- return createElement(Component, mergedProps);
+ const renderComponent = useMemo(() => {
+ return createElement(Component, { ...childProps, ref: reduxAdapterRef });
+ }, [Component, childProps, reduxAdapterRef]);
+
+ return createElement(usedContext.Provider, { value: overrideContext }, renderComponent);
};
if (options.forwardRef) {
@@ -174,3 +252,57 @@ export function connect(
return Wrapper;
};
}
+
+function useSelectorWithStore(store: ReduxStoreHandler, selector: Selector) {
+ const [, forceUpdate] = useReducer(s => s + 1, 0);
+
+ const latestSelector = useRef<(state: any) => unknown>();
+ const latestState = useRef();
+ const latestSelectedState = useRef();
+
+ const state = store.getState();
+ let selectedState: any;
+
+ // 检查选择器或状态自上次渲染以来是否发生了变更
+ if (selector !== latestSelector.current || state !== latestState.current) {
+ const newSelectedState = selector(state);
+ if (latestSelectedState.current === undefined || newSelectedState !== latestSelectedState.current) {
+ selectedState = newSelectedState;
+ } else {
+ selectedState = latestSelectedState.current;
+ }
+ } else {
+ selectedState = latestSelectedState.current;
+ }
+
+ useLayoutEffect(() => {
+ latestSelector.current = selector;
+ latestState.current = state;
+ latestSelectedState.current = selectedState;
+ });
+
+ // 订阅存储并在状态变更时更新组件
+ useLayoutEffect(() => {
+ const update = () => {
+ const newState = store.getState();
+ if (newState === latestState.current) {
+ return;
+ }
+ const newSelectedState = latestSelector.current!(newState);
+ if (newSelectedState === latestSelectedState.current) {
+ return;
+ }
+ latestSelectedState.current = newSelectedState;
+ latestState.current = newState;
+
+ forceUpdate();
+ };
+
+ update();
+
+ const unsubscribe = store.subscribe(() => update());
+ return () => unsubscribe();
+ }, [store]);
+
+ return selectedState;
+}
diff --git a/packages/inula/src/inulax/adapters/reduxSelector.ts b/packages/inula/src/inulax/adapters/reduxSelector.ts
new file mode 100644
index 00000000..b4315dce
--- /dev/null
+++ b/packages/inula/src/inulax/adapters/reduxSelector.ts
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2024 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import { isSame, shallowCompare } from '../../renderer/utils/compare';
+import type { Dispatch, ReduxAction, ReduxStoreHandler } from './redux';
+import type { MapStateToPropsP, MapDispatchToPropsP, MergePropsP, ConnectOption } from './reduxReact';
+
+type StateOrDispatch = S | Dispatch;
+
+const defaultMerge = (...args: any[]) => {
+ return Object.assign({}, ...args);
+};
+
+interface ReduxSelector {
+ dependsOnOwnProps: boolean;
+
+ (stateOrDispatch?: StateOrDispatch, ownProps?: OwnProps): any;
+}
+
+const isDependsOnOwnProps = (propsMapping: any) => {
+ return propsMapping.dependsOnOwnProps ? Boolean(propsMapping.dependsOnOwnProps) : propsMapping.length !== 1;
+};
+
+function handleMapToProps(
+ mapStateToProps?: MapStateToPropsP
+): ReduxSelector {
+ if (typeof mapStateToProps === 'function') {
+ const proxy = <
+ ReduxSelector & {
+ mapToProps: any;
+ }
+ >function mapToPropsProxy(stateOrDispatch: StateOrDispatch, ownProps?: any) {
+ return proxy.dependsOnOwnProps ? proxy.mapToProps(stateOrDispatch, ownProps) : proxy.mapToProps(stateOrDispatch);
+ };
+ proxy.dependsOnOwnProps = true;
+
+ proxy.mapToProps = function (stateOrDispatch: StateOrDispatch, ownProps: any) {
+ proxy.mapToProps = mapStateToProps;
+ proxy.dependsOnOwnProps = isDependsOnOwnProps(mapStateToProps);
+ let props = proxy(stateOrDispatch, ownProps);
+
+ if (typeof props === 'function') {
+ props.mapToProps = props;
+ proxy.dependsOnOwnProps = isDependsOnOwnProps(props);
+ props = proxy(stateOrDispatch, ownProps);
+ }
+ return props;
+ };
+ return proxy;
+ } else {
+ const selector = () => {
+ return {};
+ };
+ selector.dependsOnOwnProps = false;
+ return selector;
+ }
+}
+
+function handleMapDispatchToProps(
+ dispatch: Dispatch,
+ mapDispatchToProps?: MapDispatchToPropsP
+): ReduxSelector {
+ if (!mapDispatchToProps) {
+ const selector = () => {
+ return { dispatch: dispatch };
+ };
+ selector.dependsOnOwnProps = false;
+ return selector;
+ } else if (typeof mapDispatchToProps === 'function') {
+ return handleMapToProps(mapDispatchToProps);
+ } else {
+ const selector = () => {
+ const mappedDispatch = {};
+ Object.entries(mapDispatchToProps).forEach(([key, value]) => {
+ mappedDispatch[key] = (...args: ReduxAction[]) => {
+ dispatch(value(...args));
+ };
+ });
+ return mappedDispatch;
+ };
+ selector.dependsOnOwnProps = false;
+ return selector;
+ }
+}
+
+function getSelector(
+ store: ReduxStoreHandler,
+ {
+ mapStateToProps,
+ mapDispatchToProps,
+ mergeProps,
+ options,
+ }: {
+ mapStateToProps: MapStateToPropsP;
+ mapDispatchToProps?: MapDispatchToPropsP;
+ mergeProps?: MergePropsP;
+ options: ConnectOption;
+ }
+) {
+ const { dispatch } = store;
+ const mappedStateToProps = handleMapToProps(mapStateToProps);
+ const mappedDispatchToProps = handleMapDispatchToProps(dispatch, mapDispatchToProps);
+ const mergeMethod = mergeProps || defaultMerge;
+
+ return pureSelectorCreator(mappedStateToProps, mappedDispatchToProps, mergeMethod, dispatch, options);
+}
+
+function pureSelectorCreator(
+ mapStateToProps: ReduxSelector,
+ mapDispatchToProps: ReduxSelector,
+ mergeProps: MergePropsP,
+ dispatch: Dispatch,
+ options: ConnectOption
+) {
+ let hasRun = false;
+ let state: any;
+ let ownProps: OwnProps;
+ let stateProps: StateProps;
+ let dispatchProps: DispatchProps;
+ let mergedProps: MergedProps;
+
+ const { areStatesEqual = isSame, areOwnPropsEqual = shallowCompare, areStatePropsEqual = shallowCompare } = options;
+
+ // 首次运行该函数
+ function firstRun(initState: any, initOwnProps: OwnProps) {
+ state = initState;
+ ownProps = initOwnProps;
+ stateProps = mapStateToProps(state, ownProps);
+ dispatchProps = mapDispatchToProps(dispatch, ownProps);
+ mergedProps = mergeProps(stateProps, dispatchProps, ownProps);
+ hasRun = true;
+ return mergedProps;
+ }
+
+ function duplicateRun(newState: any, newOwnProps: OwnProps) {
+ const isStateChange = !areStatesEqual(newState, state);
+ const isPropsChange = !areOwnPropsEqual(newOwnProps, ownProps);
+ state = newState;
+ ownProps = newOwnProps;
+ if (isStateChange && isPropsChange) {
+ stateProps = mapStateToProps(state, ownProps);
+ if (mapDispatchToProps.dependsOnOwnProps) {
+ dispatchProps = mapDispatchToProps(dispatch, ownProps);
+ }
+ mergedProps = mergeProps(stateProps, dispatchProps, ownProps);
+ return mergedProps;
+ }
+ if (isPropsChange) {
+ if (mapStateToProps.dependsOnOwnProps) {
+ stateProps = mapStateToProps(state, ownProps);
+ }
+ if (mapDispatchToProps.dependsOnOwnProps) {
+ dispatchProps = mapDispatchToProps(dispatch, ownProps);
+ }
+ mergedProps = mergeProps(stateProps, dispatchProps, ownProps);
+ return mergedProps;
+ }
+ if (isStateChange) {
+ const latestStateProps = mapStateToProps(state, ownProps);
+ const isStatePropsChange = !areStatePropsEqual(latestStateProps, stateProps);
+ stateProps = latestStateProps;
+ if (isStatePropsChange) {
+ mergedProps = mergeProps(stateProps, dispatchProps, ownProps);
+ }
+ return mergedProps;
+ }
+ return mergedProps;
+ }
+
+ return function (newState: any, newOwnProps: OwnProps) {
+ return hasRun ? duplicateRun(newState, newOwnProps) : firstRun(newState, newOwnProps);
+ };
+}
+
+export { getSelector };
diff --git a/packages/inula/src/inulax/adapters/subscription.ts b/packages/inula/src/inulax/adapters/subscription.ts
new file mode 100644
index 00000000..5f5f6a94
--- /dev/null
+++ b/packages/inula/src/inulax/adapters/subscription.ts
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2024 Huawei Technologies Co.,Ltd.
+ *
+ * openInula is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ *
+ * http://license.coscl.org.cn/MulanPSL2
+ *
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+import { unstable_batchedUpdates } from '../../dom/DOMExternal';
+import { ReduxStoreHandler } from './redux';
+
+type LinkListNode = {
+ next: LinkListNode;
+ prev: LinkListNode;
+ value: T;
+} | null;
+
+type CallBack = () => void;
+
+interface ListenerManager {
+ clear(): void;
+
+ trigger(): void;
+
+ subscribe(cb: CallBack): () => void;
+}
+
+function batchUpdate(callback: () => any) {
+ unstable_batchedUpdates(callback);
+}
+
+function getLinkedList() {
+ let firstNode: LinkListNode = null;
+ let lastNode: LinkListNode = null;
+
+ function clear() {
+ firstNode = null;
+ lastNode = null;
+ }
+
+ function getIterator(): T[] {
+ const data: T[] = [];
+ let curNode = firstNode;
+ while (curNode) {
+ data.push(curNode.value);
+ curNode = curNode.next;
+ }
+ return data;
+ }
+
+ function add(element: T): NonNullable> {
+ let newNode: LinkListNode;
+ if (!firstNode || !lastNode) {
+ newNode = {
+ value: element,
+ prev: null,
+ next: null,
+ };
+ firstNode = lastNode = newNode;
+ return newNode;
+ } else {
+ newNode = {
+ value: element,
+ prev: lastNode,
+ next: null,
+ };
+ lastNode.next = newNode;
+ lastNode = newNode;
+ return newNode;
+ }
+ }
+
+ function removeNode(node: NonNullable>) {
+ if (node.next) {
+ node.next.prev = node.prev;
+ } else {
+ lastNode = node.prev;
+ }
+ if (node.prev) {
+ node.prev.next = node.next;
+ } else {
+ firstNode = node.next;
+ }
+ }
+
+ return {
+ add,
+ clear,
+ removeNode,
+ getIterator,
+ };
+}
+
+function getListenerManager(): ListenerManager {
+ const linkedList = getLinkedList();
+
+ function subscribe(cb: CallBack): () => void {
+ const listener = linkedList.add(cb);
+ return () => linkedList.removeNode(listener);
+ }
+
+ function trigger() {
+ const listeners = linkedList.getIterator();
+ batchUpdate(() => {
+ for (const listener of listeners) {
+ listener();
+ }
+ });
+ }
+
+ function clear() {
+ linkedList.clear();
+ }
+
+ return {
+ clear,
+ trigger,
+ subscribe,
+ };
+}
+
+export interface Subscription {
+ stateChange?: () => any;
+
+ addNestedSub(listener: CallBack): CallBack;
+
+ triggerNestedSubs(): void;
+
+ trySubscribe(): void;
+
+ tryUnsubscribe(): void;
+}
+
+const nullListenerStore = {} as unknown as ListenerManager;
+
+function createSubscription(store: ReduxStoreHandler, parentSub: Subscription | null = null): Subscription {
+ let unsubscribe: CallBack | undefined;
+ let listenerStore: ListenerManager = nullListenerStore;
+
+ function addNestedSub(listener: CallBack) {
+ trySubscribe();
+ return listenerStore.subscribe(listener);
+ }
+
+ function triggerNestedSubs() {
+ listenerStore.trigger();
+ }
+
+ function storeChangeHandler() {
+ if (typeof subscription.stateChange === 'function') {
+ subscription.stateChange();
+ }
+ }
+
+ function trySubscribe() {
+ if (!unsubscribe) {
+ unsubscribe = parentSub ? parentSub.addNestedSub(storeChangeHandler) : store.subscribe(storeChangeHandler);
+ listenerStore = getListenerManager();
+ }
+ }
+
+ function tryUnsubscribe() {
+ if (typeof unsubscribe === 'function') {
+ unsubscribe();
+ unsubscribe = undefined;
+ listenerStore.clear();
+ listenerStore = nullListenerStore;
+ }
+ }
+
+ const subscription: Subscription = {
+ stateChange: undefined,
+ addNestedSub,
+ triggerNestedSubs,
+ trySubscribe,
+ tryUnsubscribe,
+ };
+
+ return subscription;
+}
+
+export default createSubscription;
diff --git a/packages/inula/src/types.ts b/packages/inula/src/types.ts
index cd42165a..67db6d32 100644
--- a/packages/inula/src/types.ts
+++ b/packages/inula/src/types.ts
@@ -103,7 +103,7 @@ export interface ChildrenType {
toArray(children: InulaNode | InulaNode[]): Array>;
}
-type ForwardRef = ((inst: T | null) => void) | MutableRef | null;
+export type ForwardRef = ((inst: T | null) => void) | MutableRef | null;
export interface ForwardRefRenderFunc {
(props: P, ref: ForwardRef): InulaElement | null;