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;