fix(inulax): 修复嵌套使用connect的错误
This commit is contained in:
parent
6688cde7ab
commit
ecaaacb812
|
@ -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 (
|
||||
<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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -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<any>;
|
||||
};
|
||||
|
||||
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<StateProps, OwnProps> = (state: any, ownProps: OwnProps) => StateProps;
|
||||
type MapDispatchToPropsP<DispatchProps, OwnProps> =
|
||||
export type MapStateToPropsP<StateProps, OwnProps> = (state: any, ownProps: OwnProps) => StateProps;
|
||||
export type MapDispatchToPropsP<DispatchProps, OwnProps> =
|
||||
| { [key: string]: (...args: any[]) => ReduxAction }
|
||||
| ((dispatch: (action: ReduxAction) => any, ownProps: OwnProps) => DispatchProps);
|
||||
type MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
|
||||
export type MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
|
||||
stateProps: StateProps,
|
||||
dispatchProps: DispatchProps,
|
||||
ownProps: OwnProps
|
||||
|
@ -89,79 +115,131 @@ type MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
|
|||
type WrappedComponent<OwnProps> = (props: OwnProps) => ReturnType<typeof createElement>;
|
||||
type OriginalComponent<MergedProps> = (props: MergedProps) => ReturnType<typeof createElement>;
|
||||
type Connector<OwnProps, MergedProps> = (Component: OriginalComponent<MergedProps>) => WrappedComponent<OwnProps>;
|
||||
type ConnectOption<State = any> = {
|
||||
areStatesEqual?: (oldState: State, newState: State) => boolean;
|
||||
context?: Context;
|
||||
export type ConnectOption<State, StateProps, OwnProps> = {
|
||||
/** @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<StateProps, DispatchProps, OwnProps, MergedProps>(
|
||||
mapStateToProps: MapStateToPropsP<StateProps, OwnProps> = () => ({}) as StateProps,
|
||||
mapDispatchToProps: MapDispatchToPropsP<DispatchProps, OwnProps> = () => ({}) as DispatchProps,
|
||||
mergeProps: MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
|
||||
stateProps,
|
||||
dispatchProps,
|
||||
ownProps
|
||||
): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps }) as unknown as MergedProps,
|
||||
options: ConnectOption = {}
|
||||
mapDispatchToProps?: MapDispatchToPropsP<DispatchProps, OwnProps>,
|
||||
mergeProps?: MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps>,
|
||||
options: ConnectOption<any, StateProps, OwnProps> = {}
|
||||
): Connector<OwnProps, MergedProps> {
|
||||
//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
|
||||
const Wrapper: WrappedComponent<OwnProps> = (props: OwnProps) => {
|
||||
const store = useStore() as ReduxStoreHandler;
|
||||
const [state, setState] = useState(() => store.getState());
|
||||
const Wrapper: WrappedComponent<OwnProps> = (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<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(() => {
|
||||
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<StateProps, DispatchProps, OwnProps, MergedProps>(
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -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 };
|
|
@ -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;
|
|
@ -103,7 +103,7 @@ export interface ChildrenType {
|
|||
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> {
|
||||
(props: P, ref: ForwardRef<T>): InulaElement | null;
|
||||
|
|
Loading…
Reference in New Issue