diff --git a/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx b/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx index 38b7445b..499be389 100644 --- a/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx +++ b/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx @@ -187,7 +187,7 @@ describe('Redux adapter', () => { reduxStore.dispatch({ type: 'toggle' }); reduxStore.dispatch({ type: 'toggle' }); - expect(counter).toBe(3); // NOTE: first action is always store initialization + expect(counter).toBe(2); // execute dispatch two times, applyMiddleware was called same times }); it('Should apply multiple enhancers', async () => { @@ -226,7 +226,7 @@ describe('Redux adapter', () => { reduxStore.dispatch({ type: 'toggle' }); - expect(counter).toBe(2); // NOTE: first action is always store initialization + expect(counter).toBe(1); // execute dispatch two times, applyMiddleware was called same times expect(lastAction).toBe('toggle'); expect(middlewareCallList[0]).toBe('callCounter'); expect(middlewareCallList[1]).toBe('lastFunctionStorage'); diff --git a/packages/inula/src/inulax/CommonUtils.ts b/packages/inula/src/inulax/CommonUtils.ts index 5202a200..ccbb182f 100644 --- a/packages/inula/src/inulax/CommonUtils.ts +++ b/packages/inula/src/inulax/CommonUtils.ts @@ -67,18 +67,50 @@ export function isPromise(obj: any): boolean { return isObject(obj) && typeof obj.then === 'function'; } -export function isSame(x, y) { - if (typeof Object.is !== 'function') { - if (x === y) { - // +0 != -0 - return x !== 0 || 1 / x === 1 / y; - } else { - // NaN == NaN - return x !== x && y !== y; - } - } else { - return Object.is(x, y); +export function isSame(x: unknown, y: unknown): boolean { + // 如果两个对象是同一个引用,直接返回true + if (x === y) { + return true; } + // 如果两个对象类型不同,直接返回false + if (typeof x !== typeof y) { + return false; + } + // 如果两个对象都是null或undefined,直接返回true + if (x == null || y == null) { + return true; + } + // 如果两个对象都是基本类型,比较他们的值是否相等 + if (typeof x !== 'object') { + return x === y; + } + // 如果两个对象都是数组,比较他们的长度是否相等,然后递归比较每个元素是否相等 + if (Array.isArray(x) && Array.isArray(y)) { + if (x.length !== y.length) { + return false; + } + for (let i = 0; i < x.length; i++) { + if (!isSame(x[i], y[i])) { + return false; + } + } + return true; + } + // 两个对象都是普通对象,首先比较他们的属性数量是否相等,然后递归比较每个属性的值是否相等 + if (typeof x === 'object' && typeof y === 'object') { + const keys1 = Object.keys(x!).sort(); + const keys2 = Object.keys(y!).sort(); + if (keys1.length !== keys2.length) { + return false; + } + for (let i = 0; i < keys1.length; i++) { + if (!isSame(x![keys1[i]], y![keys2[i]])) { + return false; + } + } + return true; + } + return false; } export function getDetailedType(val: any) { diff --git a/packages/inula/src/inulax/adapters/redux.ts b/packages/inula/src/inulax/adapters/redux.ts index b996abed..97ee92f1 100644 --- a/packages/inula/src/inulax/adapters/redux.ts +++ b/packages/inula/src/inulax/adapters/redux.ts @@ -27,12 +27,12 @@ export { createDispatchHook, } from './reduxReact'; -export type ReduxStoreHandler = { - reducer: (state: any, action: { type: string }) => any; - dispatch: (action: { type: string }) => void; - getState: () => any; - subscribe: (listener: () => void) => () => void; - replaceReducer: (reducer: (state: any, action: { type: string }) => any) => void; +export type ReduxStoreHandler = { + reducer(state: T, action: { type: string }): any; + dispatch(action: { type: string }): void; + getState(): T; + subscribe(listener: () => void): () => void; + replaceReducer(reducer: (state: T, action: { type: string }) => any): void; }; export type ReduxAction = { @@ -53,6 +53,9 @@ export type ReduxMiddleware = ( type Reducer = (state: any, action: ReduxAction) => any; +type StoreCreator = (reducer: Reducer, preloadedState?: any) => ReduxStoreHandler; +type StoreEnhancer = (next: StoreCreator) => StoreCreator; + function mergeData(state, data) { if (!data) { state.stateWrapper = data; @@ -87,7 +90,7 @@ function mergeData(state, data) { state.stateWrapper = data; } -export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): ReduxStoreHandler { +export function createStore(reducer: Reducer, preloadedState?: any, enhancers?: StoreEnhancer): ReduxStoreHandler { const store = createStoreX({ id: 'defaultStore', state: { stateWrapper: preloadedState }, @@ -130,12 +133,14 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): dispatch: store.$a.dispatch, }; - enhancers && enhancers(result); - result.dispatch({ type: 'InulaX' }); store.reduxHandler = result; + if (typeof enhancers === 'function') { + return enhancers(createStore)(reducer, preloadedState); + } + return result as ReduxStoreHandler; } @@ -150,19 +155,23 @@ export function combineReducers(reducers: { [key: string]: Reducer }): Reducer { }; } -function applyMiddlewares(store: ReduxStoreHandler, middlewares: ReduxMiddleware[]): void { - middlewares = middlewares.slice(); - middlewares.reverse(); - let dispatch = store.dispatch; - middlewares.forEach(middleware => { - dispatch = middleware(store)(dispatch); - }); - store.dispatch = dispatch; +function applyMiddlewares(createStore: StoreCreator, middlewares: ReduxMiddleware[]): StoreCreator { + return (reducer, preloadedState) => { + middlewares = middlewares.slice(); + middlewares.reverse(); + const storeObj = createStore(reducer, preloadedState); + let dispatch = storeObj.dispatch; + middlewares.forEach(middleware => { + dispatch = middleware(storeObj)(dispatch); + }); + storeObj.dispatch = dispatch; + return storeObj; + }; } -export function applyMiddleware(...middlewares: ReduxMiddleware[]): (store: ReduxStoreHandler) => void { - return store => { - return applyMiddlewares(store, middlewares); +export function applyMiddleware(...middlewares: ReduxMiddleware[]): (createStore: StoreCreator) => StoreCreator { + return createStore => { + return applyMiddlewares(createStore, middlewares); }; } @@ -170,7 +179,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) => any; +type Dispatch = (action: ReduxAction) => any; export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators { const boundActionCreators = {}; @@ -183,12 +192,12 @@ export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dis return boundActionCreators; } -export function compose(...middlewares: ReduxMiddleware[]) { - return (store: ReduxStoreHandler, extraArgument: any) => { - let val; - middlewares.reverse().forEach((middleware: ReduxMiddleware, index) => { +export function compose(...middlewares: ((...args: any[]) => any)[]): (...args: any[]) => T { + return (...args) => { + let val: any; + middlewares.reverse().forEach((middleware, index) => { if (!index) { - val = middleware(store, extraArgument); + val = middleware(...args); return; } val = middleware(val); diff --git a/packages/inula/src/inulax/adapters/reduxReact.ts b/packages/inula/src/inulax/adapters/reduxReact.ts index 160dec37..32eff8d0 100644 --- a/packages/inula/src/inulax/adapters/reduxReact.ts +++ b/packages/inula/src/inulax/adapters/reduxReact.ts @@ -41,29 +41,27 @@ export function createStoreHook(context: Context): () => ReduxStoreHandler { }; } -export function createSelectorHook(context: Context): (selector?: (any) => any) => any { - const store = createStoreHook(context)() as unknown as ReduxStoreHandler; - return function (selector = state => state) { - const [b, fr] = useState(false); +export function createSelectorHook(context: Context): (selector?: ((state: unknown) => any) | undefined) => any { + const store = createStoreHook(context)(); + return function useSelector(selector = state => state) { + const [state, setState] = useState(() => store.getState()); useEffect(() => { - const unsubscribe = store.subscribe(() => fr(!b)); - return () => { - unsubscribe(); - }; - }); + const unsubscribe = store.subscribe(() => { + setState(store.getState()); + }); + return () => unsubscribe(); + }, []); - return selector(store.getState()); + return selector(state); }; } export function createDispatchHook(context: Context): () => BoundActionCreator { - const store = createStoreHook(context)() as unknown as ReduxStoreHandler; - return function () { - return action => { - store.dispatch(action); - }; - }.bind(store); + const store = createStoreHook(context)(); + return function useDispatch() { + return store.dispatch; + }; } export const useSelector = selector => { @@ -94,7 +92,7 @@ type Connector = (Component: OriginalComponent = { areStatesEqual?: (oldState: State, newState: State) => boolean; context?: Context; - forwardRef?: boolean + forwardRef?: boolean; } export function connect( @@ -105,7 +103,7 @@ export function connect( dispatchProps, ownProps ): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as unknown as MergedProps), - options: ConnectOption, + options?: ConnectOption ): Connector { if (!options) { options = {}; @@ -117,37 +115,31 @@ export function connect( //this component should mimic original type of component used const Wrapper: WrappedComponent = (props: OwnProps) => { - const [f, forceReload] = useState(true); - - const store = useStore() as unknown as ReduxStoreHandler; + const store = useStore() as ReduxStoreHandler; + const [state, setState] = useState(() => store.getState()); useEffect(() => { - const unsubscribe = store.subscribe(() => forceReload(!f)); - return () => { - unsubscribe(); - }; - }); + const unsubscribe = store.subscribe(() => { + setState(store.getState()); + }); + return () => unsubscribe(); + }, []); - const previous = useRef({ + const previous = useRef<{ state: { [key: string]: any }; mappedState: StateProps }>({ state: {}, - mappedState: {}, - }) as { - current: { - state: { [key: string]: any }; - mappedState: StateProps; - }; - }; + mappedState: {} as StateProps, + }); let mappedState: StateProps; if (options?.areStatesEqual) { - if (options.areStatesEqual(previous.current.state, store.getState())) { + if (options.areStatesEqual(previous.current.state, state)) { mappedState = previous.current.mappedState as StateProps; } else { - mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps); + mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps); previous.current.mappedState = mappedState; } } else { - mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps); + mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps); previous.current.mappedState = mappedState; } let mappedDispatch: DispatchProps = {} as DispatchProps; @@ -156,12 +148,14 @@ export function connect( 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) => { @@ -169,13 +163,12 @@ export function connect( }) )(mappedState, mappedDispatch, props); - previous.current.state = store.getState(); + previous.current.state = state; - const node = createElement(Component, mergedProps); - return node; + return createElement(Component, mergedProps); }; - if (options.forwardRef) { + if (options?.forwardRef) { const forwarded = forwardRef((props, ref) => { return Wrapper({ ...props, ref: ref }); }); diff --git a/packages/inula/src/inulax/adapters/reduxThunk.ts b/packages/inula/src/inulax/adapters/reduxThunk.ts index cd14fee6..7eaaf227 100644 --- a/packages/inula/src/inulax/adapters/reduxThunk.ts +++ b/packages/inula/src/inulax/adapters/reduxThunk.ts @@ -35,5 +35,5 @@ function createThunkMiddleware(extraArgument?: any): ReduxMiddleware { } export const thunk = createThunkMiddleware(); -// @ts-ignore -thunk.withExtraArgument = createThunkMiddleware; + +export const withExtraArgument = createThunkMiddleware;