fix(inulax): 修复嵌套使用connect的错误

This commit is contained in:
huangxuan 2024-04-03 15:41:15 +08:00
parent 6688cde7ab
commit ecaaacb812
No known key found for this signature in database
GPG Key ID: E79F50C67022565D
6 changed files with 679 additions and 82 deletions

View File

@ -375,4 +375,93 @@ describe('Redux/React binding adapter', () => {
expect(getE(BUTTON).innerHTML).toBe('1'); expect(getE(BUTTON).innerHTML).toBe('1');
expect(getE(BUTTON2).innerHTML).toBe('true'); 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 (
<div>
<h2>Child Component</h2>
<p id="child">{childData}</p>
</div>
);
};
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 (
<div>
<h1>Parent Component</h1>
<p id="parent">{parentData}</p>
<Child />
</div>
);
};
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(
<Provider store={store}>
<Parent />
</Provider>,
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');
});
}); });

View File

@ -184,7 +184,7 @@ type ActionCreator = (...params: any[]) => ReduxAction;
type ActionCreators = { [key: string]: ActionCreator }; type ActionCreators = { [key: string]: ActionCreator };
export type BoundActionCreator = (...params: any[]) => void; export type BoundActionCreator = (...params: any[]) => void;
type BoundActionCreators = { [key: string]: BoundActionCreator }; type BoundActionCreators = { [key: string]: BoundActionCreator };
type Dispatch = (action: ReduxAction) => any; export type Dispatch = (action: ReduxAction) => any;
export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators { export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators {
const boundActionCreators = {}; const boundActionCreators = {};

View File

@ -13,14 +13,28 @@
* See the Mulan PSL v2 for more details. * 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 { createContext } from '../../renderer/components/context/CreateContext';
import { createElement } from '../../external/JSXElement'; 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 { 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 Context = typeof DefaultContext;
type Selector = (state: unknown) => unknown;
type ConnectProps = {
store: ReduxStoreHandler;
context?: Context;
};
type WrapperInnerProps = {
reduxAdapterRef?: ForwardRef<any>;
};
export function Provider({ export function Provider({
store, store,
@ -28,32 +42,44 @@ export function Provider({
children, children,
}: { }: {
store: ReduxStoreHandler; store: ReduxStoreHandler;
context: Context; context?: Context;
children?: any[]; 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; 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 { export function createStoreHook(context: Context): () => ReduxStoreHandler {
return () => { 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)(); const store = createStoreHook(context)();
return function useSelector(selector = state => state) { return function useSelector(selector: Selector = state => state) {
const [state, setState] = useState(() => store.getState()); return useSelectorWithStore(store, selector);
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setState(store.getState());
});
return () => unsubscribe();
}, []);
return selector(state);
}; };
} }
@ -64,7 +90,7 @@ export function createDispatchHook(context: Context): () => BoundActionCreator {
}; };
} }
export const useSelector = selector => { export const useSelector = (selector: Selector) => {
return createSelectorHook(DefaultContext)(selector); return createSelectorHook(DefaultContext)(selector);
}; };
@ -76,11 +102,11 @@ export const useStore = () => {
return createStoreHook(DefaultContext)(); return createStoreHook(DefaultContext)();
}; };
type MapStateToPropsP<StateProps, OwnProps> = (state: any, ownProps: OwnProps) => StateProps; export type MapStateToPropsP<StateProps, OwnProps> = (state: any, ownProps: OwnProps) => StateProps;
type MapDispatchToPropsP<DispatchProps, OwnProps> = export type MapDispatchToPropsP<DispatchProps, OwnProps> =
| { [key: string]: (...args: any[]) => ReduxAction } | { [key: string]: (...args: any[]) => ReduxAction }
| ((dispatch: (action: ReduxAction) => any, ownProps: OwnProps) => DispatchProps); | ((dispatch: (action: ReduxAction) => any, ownProps: OwnProps) => DispatchProps);
type MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = ( export type MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
stateProps: StateProps, stateProps: StateProps,
dispatchProps: DispatchProps, dispatchProps: DispatchProps,
ownProps: OwnProps ownProps: OwnProps
@ -89,79 +115,131 @@ type MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
type WrappedComponent<OwnProps> = (props: OwnProps) => ReturnType<typeof createElement>; type WrappedComponent<OwnProps> = (props: OwnProps) => ReturnType<typeof createElement>;
type OriginalComponent<MergedProps> = (props: MergedProps) => ReturnType<typeof createElement>; type OriginalComponent<MergedProps> = (props: MergedProps) => ReturnType<typeof createElement>;
type Connector<OwnProps, MergedProps> = (Component: OriginalComponent<MergedProps>) => WrappedComponent<OwnProps>; type Connector<OwnProps, MergedProps> = (Component: OriginalComponent<MergedProps>) => WrappedComponent<OwnProps>;
type ConnectOption<State = any> = { export type ConnectOption<State, StateProps, OwnProps> = {
areStatesEqual?: (oldState: State, newState: State) => boolean; /** @deprecated */
context?: Context; prue?: boolean;
forwardRef?: 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<StateProps, DispatchProps, OwnProps, MergedProps>( export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
mapStateToProps: MapStateToPropsP<StateProps, OwnProps> = () => ({}) as StateProps, mapStateToProps: MapStateToPropsP<StateProps, OwnProps> = () => ({}) as StateProps,
mapDispatchToProps: MapDispatchToPropsP<DispatchProps, OwnProps> = () => ({}) as DispatchProps, mapDispatchToProps?: MapDispatchToPropsP<DispatchProps, OwnProps>,
mergeProps: MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = ( mergeProps?: MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps>,
stateProps, options: ConnectOption<any, StateProps, OwnProps> = {}
dispatchProps,
ownProps
): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps }) as unknown as MergedProps,
options: ConnectOption = {}
): Connector<OwnProps, MergedProps> { ): Connector<OwnProps, MergedProps> {
//this component should bear the type returned from mapping functions //this component should bear the type returned from mapping functions
return (Component: OriginalComponent<MergedProps>): WrappedComponent<OwnProps> => {
const useStore = createStoreHook(options.context || DefaultContext);
const selectorOptions = {
mapStateToProps,
mapDispatchToProps,
mergeProps,
options,
};
const { context: storeContext = DefaultContext } = options;
return (Component: OriginalComponent<MergedProps>): WrappedComponent<OwnProps> => {
//this component should mimic original type of component used //this component should mimic original type of component used
const Wrapper: WrappedComponent<OwnProps> = (props: OwnProps) => { const Wrapper: WrappedComponent<OwnProps> = (props: OwnProps & ConnectProps & WrapperInnerProps) => {
const store = useStore() as ReduxStoreHandler; const [, forceUpdate] = useReducer(s => s + 1, 0);
const [state, setState] = useState(() => store.getState());
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<any>();
const latestWrappedProps = useRef(wrappedProps);
const childPropsFormStore = useRef<any>();
const isRendering = useRef<boolean>(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(() => { useEffect(() => {
const unsubscribe = store.subscribe(() => { latestChildProps.current = childProps;
setState(store.getState()); latestWrappedProps.current = wrappedProps;
}); isRendering.current = false;
return () => unsubscribe(); if (childPropsFormStore.current) {
}, []); childPropsFormStore.current = null;
triggerNestedSubs();
const previous = useRef<{ state: { [key: string]: any }; mappedState: StateProps }>({ }
state: {},
mappedState: {} as StateProps,
}); });
let mappedState: StateProps; useEffect(() => {
if (options.areStatesEqual) { let isUnsubscribe = false;
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);
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) { if (options.forwardRef) {
@ -174,3 +252,57 @@ export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
return Wrapper; return Wrapper;
}; };
} }
function useSelectorWithStore(store: ReduxStoreHandler, selector: Selector) {
const [, forceUpdate] = useReducer(s => s + 1, 0);
const latestSelector = useRef<(state: any) => unknown>();
const latestState = useRef<any>();
const latestSelectedState = useRef<any>();
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;
}

View File

@ -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 extends ReduxAction = ReduxAction> = S | Dispatch;
const defaultMerge = (...args: any[]) => {
return Object.assign({}, ...args);
};
interface ReduxSelector<OwnProps = unknown> {
dependsOnOwnProps: boolean;
(stateOrDispatch?: StateOrDispatch, ownProps?: OwnProps): any;
}
const isDependsOnOwnProps = (propsMapping: any) => {
return propsMapping.dependsOnOwnProps ? Boolean(propsMapping.dependsOnOwnProps) : propsMapping.length !== 1;
};
function handleMapToProps<StateProps, OwnProps>(
mapStateToProps?: MapStateToPropsP<StateProps, OwnProps>
): 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<DispatchProps, OwnProps>(
dispatch: Dispatch,
mapDispatchToProps?: MapDispatchToPropsP<DispatchProps, OwnProps>
): 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<StateProps, DispatchProps, OwnProps, MergedProps>(
store: ReduxStoreHandler,
{
mapStateToProps,
mapDispatchToProps,
mergeProps,
options,
}: {
mapStateToProps: MapStateToPropsP<StateProps, OwnProps>;
mapDispatchToProps?: MapDispatchToPropsP<DispatchProps, OwnProps>;
mergeProps?: MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps>;
options: ConnectOption<unknown, StateProps, OwnProps>;
}
) {
const { dispatch } = store;
const mappedStateToProps = handleMapToProps<StateProps, OwnProps>(mapStateToProps);
const mappedDispatchToProps = handleMapDispatchToProps<DispatchProps, OwnProps>(dispatch, mapDispatchToProps);
const mergeMethod = mergeProps || defaultMerge;
return pureSelectorCreator(mappedStateToProps, mappedDispatchToProps, mergeMethod, dispatch, options);
}
function pureSelectorCreator<StateProps, DispatchProps, OwnProps, MergedProps>(
mapStateToProps: ReduxSelector,
mapDispatchToProps: ReduxSelector,
mergeProps: MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps>,
dispatch: Dispatch,
options: ConnectOption<unknown, StateProps, OwnProps>
) {
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 };

View File

@ -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<T> = {
next: LinkListNode<T>;
prev: LinkListNode<T>;
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<T>() {
let firstNode: LinkListNode<T> = null;
let lastNode: LinkListNode<T> = 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<LinkListNode<T>> {
let newNode: LinkListNode<T>;
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<LinkListNode<T>>) {
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<CallBack>();
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;

View File

@ -103,7 +103,7 @@ export interface ChildrenType {
toArray(children: InulaNode | InulaNode[]): Array<Exclude<InulaNode, boolean | null | undefined>>; toArray(children: InulaNode | InulaNode[]): Array<Exclude<InulaNode, boolean | null | undefined>>;
} }
type ForwardRef<T> = ((inst: T | null) => void) | MutableRef<T | null> | null; export type ForwardRef<T> = ((inst: T | null) => void) | MutableRef<T | null> | null;
export interface ForwardRefRenderFunc<T, P = KVObject> { export interface ForwardRefRenderFunc<T, P = KVObject> {
(props: P, ref: ForwardRef<T>): InulaElement | null; (props: P, ref: ForwardRef<T>): InulaElement | null;