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(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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 = {};
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>>;
|
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;
|
||||||
|
|
Loading…
Reference in New Issue