Redux is a web front-end state management library, common to all front-end pages, and does not depend on any framework. It is a pure js project. Its principle and source code are relatively simple. We must first understand the redux source code, and then look at the redux-toolkit source code. Only then can we understand why the source code of redux-toolkit is written in that way, and then we can understand the source code of react-redux in combination with the react source code. This time we will first understand the source code of redux.
The types files are all interfaces or type definitions of ts, and the utils folder is all utility classes. You can also see what these tools do from the file names
The entry file is index.ts, which is the function that exports several other files. The main code is as follows:
exportdefaultfunction createStore< S, A extendsAction, Ext = {}, StateExt = never >( reducer: Reducer<S, A>, preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>, enhancer?: StoreEnhancer<Ext, StateExt> ): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
This is the declaration of the function. The parameters are what we usually pass in when we use createStore, the reducer, the initialization state, and the last enhancer that we don’t usually use.
First, some checks were carried out inside the function
if ( (typeof preloadedState = 'function' && typeof enhancer = 'function') || (typeof enhancer = 'function' && typeofarguments[3] = 'function') ) { thrownewError( 'It looks like you are passing several store enhancers to ' + 'createStore(). This is not supported. Instead, compose them ' + 'together to a single function. See https://redux.js.org/tutorials/fundamentals/part-4-store#creating-a-store-with-enhancers for an example.' ) }
//The previous code here guarantees that the last two parameters passed are not all function, and if there are only two parameters in total, and the second parameter is function, then the second parameter is considered to be an enhancer instead of the initialization state
//If there is an enhancer, return the createStore after the enhancer. if (typeof enhancer ! 'undefined') { if (typeof enhancer ! 'function') { thrownewError( `Expected the enhancer to be a function. Instead, received: '${kindOf( enhancer )}'` ) }
if (typeof reducer ! 'function') { thrownewError( `Expected the root reducer to be a function. Instead, received: '${kindOf( reducer )}'` ) }
Several internal variables:
1 2 3 4 5
let currentReducer = reducer let currentState = preloadedState as S letcurrentListeners: (() =>void)[] | null = [] let nextListeners = currentListeners let isDispatching = false
Internal tool function
getState
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/** * Reads the state tree managed by the store. * * @returns The current state tree of your application. */ functiongetState(): S { if (isDispatching) { thrownewError( 'You may not call store.getState() while the reducer is executing. ' + 'The reducer has already received the state as an argument. ' + 'Pass it down from the top reducer instead of reading it from the store.' ) }
return currentState as S }
subscribe
ensureCanMutateNextListeners function is used to assign a shallow copy of currentListeners to nextListeners, which ensures that the sequential execution of currentListeners does not affect the addition of new listeners for nextListeners
subscribe adds a new listener to nextListeners and returns a closure function to cancel the subscription
/** * This makes a shallow copy of currentListeners so we can use * nextListeners as a temporary list while dispatching. * * This prevents any bugs around consumers calling * subscribe/unsubscribe in the middle of a dispatch. */ functionensureCanMutateNextListeners() { if (nextListeners = currentListeners) { nextListeners = currentListeners.slice() } }
/** * Adds a change listener. It will be called any time an action is dispatched, * and some part of the state tree may potentially have changed. You may then * call `getState()` to read the current state tree inside the callback. * * You may call `dispatch()` from a change listener, with the following * caveats: * * 1. The subscriptions are snapshotted just before every `dispatch()` call. * If you subscribe or unsubscribe while the listeners are being invoked, this * will not have any effect on the `dispatch()` that is currently in progress. * However, the next `dispatch()` call, whether nested or not, will use a more * recent snapshot of the subscription list. * * 2. The listener should not expect to see all state changes, as the state * might have been updated multiple times during a nested `dispatch()` before * the listener is called. It is, however, guaranteed that all subscribers * registered before the `dispatch()` started will be called with the latest * state by the time it exits. * * @param listener A callback to be invoked on every dispatch. * @returns A function to remove this change listener. */ functionsubscribe(listener: () => void) { if (typeof listener ! 'function') { thrownewError( `Expected the listener to be a function. Instead, received: '${kindOf( listener )}'` ) }
if (isDispatching) { thrownewError( 'You may not call store.subscribe() while the reducer is executing. ' + 'If you would like to be notified after the store has been updated, subscribe from a ' + 'component and invoke store.getState() in the callback to access the latest state. ' + 'See https://redux.js.org/api/store#subscribelistener for more details.' ) }
returnfunctionunsubscribe() { if (!isSubscribed) { return }
if (isDispatching) { thrownewError( 'You may not unsubscribe from a store listener while the reducer is executing. ' + 'See https://redux.js.org/api/store#subscribelistener for more details.' ) }
/** * Dispatches an action. It is the only way to trigger a state change. * * The `reducer` function, used to create the store, will be called with the * current state tree and the given `action`. Its return value will * be considered the **next** state of the tree, and the change listeners * will be notified. * * The base implementation only supports plain object actions. If you want to * dispatch a Promise, an Observable, a thunk, or something else, you need to * wrap your store creating function into the corresponding middleware. For * example, see the documentation for the `redux-thunk` package. Even the * middleware will eventually dispatch plain object actions using this method. * * @param action A plain object representing “what changed”. It is * a good idea to keep actions serializable so you can record and replay user * sessions, or use the time travelling `redux-devtools`. An action must have * a `type` property which may not be `undefined`. It is a good idea to use * string constants for action types. * * @returns For convenience, the same action object you dispatched. * * Note that, if you use a custom middleware, it may wrap `dispatch()` to * return something else (for example, a Promise you can await). */ functiondispatch(action: A) { if (!isPlainObject(action)) { thrownewError( `Actions must be plain objects. Instead, the actual type was: '${kindOf( action )}'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.` ) }
if (typeof action.type = 'undefined') { thrownewError( 'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.' ) }
if (isDispatching) { thrownewError('Reducers may not dispatch actions.') }
/** * Replaces the reducer currently used by the store to calculate the state. * * You might need this if your app implements code splitting and you want to * load some of the reducers dynamically. You might also need this if you * implement a hot reloading mechanism for Redux. * * @param nextReducer The reducer for the store to use instead. * @returns The same store instance with a new reducer in place. */ function replaceReducer<NewState, NewActionsextends A>( nextReducer: Reducer<NewState, NewActions> ): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext { if (typeof nextReducer ! 'function') { thrownewError( `Expected the nextReducer to be a function. Instead, received: '${kindOf( nextReducer )}` ) }
// TODO: do this more elegantly ;(currentReducer asunknownasReducer<NewState, NewActions>) = nextReducer
// This action has a similar effect to ActionTypes.INIT. // Any reducers that existed in both the new and old rootReducer // will receive the previous state. This effectively populates // the new state tree with any relevant data from the old one. dispatch({ type: ActionTypes.REPLACE } as A) // change the type of the store by casting it to the new store return store asunknownasStore< ExtendState<NewState, StateExt>, NewActions, StateExt, Ext > & Ext }
/** * Interoperability point for observable/reactive libraries. * @returns A minimal observable of state changes. * For more information, see the observable proposal: * https://github.com/tc39/proposal-observable */ functionobservable() { const outerSubscribe = subscribe return { /** * The minimal observable subscription method. * @param observer Any object that can be used as an observer. * The observer object should have a `next` method. * @returns An object with an `unsubscribe` method that can * be used to unsubscribe the observable from the store, and prevent further * emission of values from the observable. */ subscribe(observer: unknown) { if (typeof observer ! 'object' || observer = null) { thrownewTypeError( `Expected the observer to be an object. Instead, received: '${kindOf( observer )}'` ) }
First, automatically dispatch an action: {type: ActionTypes. INIT}, and then return.
1 2 3 4 5 6 7 8 9 10 11 12 13
// When a store is created, an "INIT" action is dispatched so that every // reducer returns their initial state. This effectively populates // the initial state tree. dispatch({ type: ActionTypes.INIT } as A)
const store = { dispatch: dispatch asDispatch<A>, subscribe, getState, replaceReducer, [$$observable]: observable } asunknownasStore<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext return store
So, it is true that as the doc said, the state and action in the store are pure objects, but this pure object has some specifications. You can not follow this specification, but if you do not follow this specification, you cannot operate normally.
compose
There is only one method in this file, which is very simple, that is, to turn a string of functions into a function A, A is actually to execute the string of functions in turn.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
exportdefaultfunctioncompose(...funcs: Function[]) { if (funcs.length = 0) { // infer the argument type so it is usable in inference down the line return <T>(arg: T) => arg }
There are two tool functions inside this function, namely’getUnexpectedStateShapeWarningMessage ‘and’assertReducerShape’. Looking at the function name, we know what to do. We focus on the’combineReducers’ function, which probably does the following things:
According to the incoming reducers, get all the keys of the object, then iterate over each key. If the value corresponding to the key is a function, then stuff the key and value into the finalReducers object Returns a function that internally iterates over each key in finalReducers and uses the reducer corresponding to finalReducers [key] to compute the latest state [key]
/** * Turns an object whose values are different reducer functions, into a single * reducer function. It will call every child reducer, and gather their results * into a single state object, whose keys correspond to the keys of the passed * reducer functions. * * @template S Combined state object type. * * @param reducers An object whose values correspond to different reducer * functions that need to be combined into one. One handy way to obtain it * is to use ES6 `import * as reducers` syntax. The reducers may never * return undefined for any action. Instead, they should return their * initial state if the state passed to them was undefined, and the current * state for any unrecognized action. * * @returns A reducer function that invokes every reducer inside the passed * object, and builds a state object with the same shape. */ exportdefaultfunctioncombineReducers(reducers: ReducersMapObject) { const reducerKeys = Object.keys(reducers) constfinalReducers: ReducersMapObject = {} for (let i = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i]
if (process.env.NODE_ENV ! 'production') { if (typeof reducers[key] = 'undefined') { warning(`No reducer provided for key "${key}"`) } }
// This is used to make sure we don't warn about the same // keys multiple times. letunexpectedKeyCache: { [key: string]: true } if (process.env.NODE_ENV ! 'production') { unexpectedKeyCache = {} }
if (process.env.NODE_ENV ! 'production') { const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, unexpectedKeyCache ) if (warningMessage) { warning(warningMessage) } }
let hasChanged = false constnextState: StateFromReducersMapObject<typeof reducers> = {} for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i] const reducer = finalReducers[key] const previousStateForKey = state[key] const nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey = 'undefined') { const actionType = action && action.type thrownewError( `When called with an action of type ${ actionType ? `"${String(actionType)}"` : '(unknown type)' }, the slice reducer for key "${key}" returned undefined. ` + `To ignore an action, you must explicitly return the previous state. ` + `If you want this reducer to hold no value, you can return null instead of undefined.` ) } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey ! previousStateForKey } hasChanged = hasChanged || finalReducerKeys.length ! Object.keys(state).length return hasChanged ? nextState : state } }
bindActionCreators
This function is not the logic of the redux core, but a tool function provided to users. For example, react-redux uses this function. We will talk about how to use it later. First, we will roughly understand what it does
The two parameters of this function, actionCreators and dispatch function, are used to convert the actionCreator corresponding to each key in the actionCreators into a call to the dispatch function, and then return
if (typeof actionCreators ! 'object' || actionCreators = null) { thrownewError( `bindActionCreators expected an object or a function, but instead received: '${kindOf( actionCreators )}'. ` + `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?` ) }
constboundActionCreators: ActionCreatorsMapObject = {} for (const key in actionCreators) { const actionCreator = actionCreators[key] if (typeof actionCreator = 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators }
applyMiddleware
This function is an application of function currying. It passes in middlewares, which is an array of functions, and then returns a new function. The new function saves middlewares as a closure. When the new function is executed, it will first call the parameters of the new function: createStore to create a store, although the dispatch function function is strengthened, all middleware functions are executed before the real dispatch is executed, that is, 'const chain = middlewares.map (middleware = > middleware (middlewareAPI)); dispatch = Compose < typeof dispatch > (… chain) (store.dispatch) 'these two lines of function
We can also see that the applyMidleware function returns StoreEnhancer, which is the third parameter of our createStore function. ApplyMiddleware is the only enhancer provided by redux.
exportdefaultfunctionapplyMiddleware( ...middlewares: Middleware[] ): StoreEnhancer<any> { return(createStore: StoreEnhancerStoreCreator) => <S, A extendsAnyAction>( reducer: Reducer<S, A>, preloadedState?: PreloadedState<S> ) => { const store = createStore(reducer, preloadedState) letdispatch: Dispatch = () => { thrownewError( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' ) }