Entry file index.js
import createStore from './createStore' import combineReducers from './combineReducers' import bindActionCreators from './bindActionCreators' import applyMiddleware from './applyMiddleware' import compose from './compose' import warning /utils/warning' import __DO_NOT_USE__ActionTypes from './utils/actionTypes' /* * Warning */ function isCrushed() {} if (process.enp.node_env! == 'production' && typeof isCrushed.name === 'string' && isCrushed.name ! == 'isCrushed' ) { warning( 'You are currently using minified code outside of NODE_ENV === "production". ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' + 'to ensure you have the correct code for your production build.' ) } export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose, __DO_NOT_USE__ActionTypes }Copy the code
The entry file is simple and exposes several commonly used apis
So let’s start with createStore, which we’re most familiar with
createStore.js
I’m going to delete the comments for those of you who are interested, but it’s still helpful
Determine the type of the input parameter
/ / into the parameter types if ((typeof preloadedState = = = 'function' && typeof enhancer = = = 'function') | | (typeof enhancer = = = 'function' && typeof arguments[3] === 'function') ) { throw new Error( 'It looks like you are passing several store enhancers to ' + 'createStore(). This is not supported. Instead, Compose them '+ 'together to a single function.')} Reducer (Reducer, enhancer) if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState PreloadedState = undefined} // enhancer = function if (typeof enhancer! == 'undefined') { if (typeof enhancer ! == 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, PreloadedState) / / here to write down} / / reducer for function if (typeof reducer! == 'function') { throw new Error('Expected the reducer to be a function.') }Copy the code
You can see that this is mostly fault tolerant logic
return enhancer(createStore)(reducer, preloadedState)
Copy the code
I’m going to ignore this for a second and I’m going to move on
Declare required variables
CurrentReducer = reducer // Receive incoming initState let currentState = preloadedState // Listen on function queue let CurrentListeners = [] // Let nextListeners = currentListeners // The switches are often isolated lets to maintain data consistency and behavior isDispatching = falseCopy the code
Here you can see that some variables are declared, and it’s easy to think of observer mode, and single mode
Function ensureCanMutateNextListeners
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
Copy the code
The notice is a tool function that executes a slice on the process.
GetState () function
Function getState() {if (isDispatching) {throw new Error(' When the reducer is executed. Cannot call store.getState. '+' Reducer has received parameters as state. '+' to be processed in reducer instead of reading from getState ')} return currentState}Copy the code
The state function is read, wherein the fault-tolerant logic is that getState cannot be called when dispatching dispatching, which should be to ensure the consistency of data. However, after reading the redux source code, it can be seen that dispatch is synchronous and consistent. Consistency and synchronicity here refer to that from the beginning of reducer processing parameters to the end of reducer processing, without any interference from other scopes (Reducer should be a pure function).
The subscribe () function
Function subscribe(listener) {// The listener should be a callback if (typeof listener! == 'function') {throw new Error('Expected the listener to be a function.')} // The fault tolerant logic cannot execute subscribe if (isDispatching) { throw new Error( '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-reference/store#subscribelistener for more details.')} // identify the successful subscribed let isSubscribed = true // Ensure that listeners queue isolation ensureCanMutateNextListeners () / / add a callback to listening to the queue nextListeners. Push (the listener) return function Unsubscribe () {/* The unsubscribe function is returned directly if (! Subscribed) {return} if (isDispatching) {throw new Error('You may not unsubscribe from a store listener while the reducer is executing. ' + 'See https://redux.js.org/api-reference/store#subscribelistener for more details.' ) } // flag unsubscribe, If multiple calls unsubscribe only first executed isSubscribed = false / / make sure listeners queue isolation ensureCanMutateNextListeners () / / to the characteristics of the closure Callback const index = nextListeners. IndexOf (listener) // Delete callback nextListeners. Splice (index, CurrentListeners = null}}Copy the code
You can see that this is a subscription function that returns a function that unsubscribes
1. The closure principle is used to accomplish the result. If the incoming callback is not function, the cancellation of subscription does not take effect in the case of dispatching dispatching with subscribe And unsubscribe returned by multiple calls. Only fault tolerant processing is applied the first time (isSubscribed variable).
2. Get the subscription callback in the returned unsubscribe function using the closure
3. Ensure the isolation and consistency of listening queue callback
Function dispatch
function dispatch(action) { if (! IsPlainObject (Action)) {throw new Error(' Action should handle asynchronous action for a simple object' + 'using custom middleware.')} if (typeof Action.type === 'undefined') {throw new Error(' Action must have a type attribute '+' ')} if (isDispatching) {throw new Error(' Reducer is handling but cannot dispatch.')} try {isDispatching = true currentState = currentReducer(currentState, action) } finally { isDispatching = false } const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } return action }Copy the code
The primitive dispatch function, why primitive? The answer lies in middleware. Let’s take a look at this dispatch function first
1. Determine if the action passed is a simple object, an isPlainObject function used here
Export default function isPlainObject(obj) {return false */ if (typeof obj! = = 'object' | | obj = = = null) return false / * decide whether the object is a simple object Namely literal creation const obj = {} constructor creates const obj = new Object () Object. Create const obj = object. Create (object. Prototype) __proto__ === object. prototype =true Object.prototype.__proto__ === null =true that is, the while loop executes once Prototype =true object.getProtoTypeof (proto) === null */ let proto = obj while (Object.getPrototypeOf(proto) ! == null) { proto = Object.getPrototypeOf(proto) } return Object.getPrototypeOf(obj) === proto }Copy the code
2. The action type is mandatory
3. Check whether dispatching is dispatching can be understood as fault-tolerant logic
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
Copy the code
Call the Reducer incoming action to return the new state
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
Copy the code
Call listener queue in turn
Function observables
function observable() { const outerSubscribe = subscribe return { subscribe(observer) { if (typeof observer ! == 'object' || observer === null) { throw new TypeError('Expected the observer to be an object.') } function observeState() { if (observer.next) { observer.next(getState()) } } observeState() const unsubscribe = outerSubscribe(observeState) return { unsubscribe } }, [$$observable]() { return this } } }Copy the code
The API is not directly exposed to developers
CreateStore is done here, but
One remaining issue was that when enhancer was passed in (applyMiddleware), enhancer was called, createStore itself was passed in, and reducer and preloadedState were passed in
Applymiddleware.js (Redux’s Powerful Secret)
/** * Create store of redux, apply middleware to dispatch ** convenient for various task expression ** See redux-thunk as an example ** Since middleware can be asynchronous, So it should be placed first * * each middleware will be given getState Dispatch as a parameter * @param {... * @returns {Function} an enhanced store. */ export default Function applyMiddleware(... middlewares) { return createStore => (... args) => { const store = createStore(... Args) let Dispatch = () => {throw new Error(' Dispatch cannot be scheduled when initialized.' + 'Other middleware should not use this dispatch.')} const middlewareAPI = { getState: store.getState, dispatch: (... args) => dispatch(... args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch) return { ... store, dispatch } } }Copy the code
We analyze it line by line:
Receive middleware function that returns a chain function that receives createStore and Args (Reducer, preloadedState) and passes the reducer and preloadedState to createStore
Initialize the dispatch. If the dispatch is in the initialization state, error will be generated, declaring that an object getState is store and getState Dispatch is the dispatch declared above
The map passes in the Middlewares array and calls the object just declared
(key)
Use the compose function to extend the middlewares after the incoming map call
compose
export default function compose(... funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (... args) => a(b(... args))) }Copy the code
The reset parameter is an array, which uses the array reduce method to generate a composite function.
The compound function can be understood as calling function B with an argument and then calling function A
The composition function is then called to pass in the raw dispatch returned by createStore
Reassign the return value to the dispatch that just declared error
Return an object that extends the original Store, and the enhanced dispatch overwrites the original Dispatch even if you use Applymiddleware and get the enhanced Dispatch
Now, this might be a little bit confusing but I’m not going to rush into what the compose function does at the end and why is that
combineReducers.js
/** * Convert the different reducer functions into a single reducer function * call each sub-Reducer and collect the results * convert the reducer into a single state object, His key corresponds to the passed key */ export default Function combineReducers(reducers) {const reducerKeys = object.keys (reducers) const finalReducers = {} 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}"`) } } if (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key] } } const finalReducerKeys = Obj. keys(finalReducers) /* Obtain value reducer */ let unexpectedKeyCache if (process.env.node_env! == 'production') { unexpectedKeyCache = {} } let shapeAssertionError try { assertReducerShape(finalReducers) } catch (e) { shapeAssertionError = e } return function combination(state = {}, action) { if (shapeAssertionError) { throw shapeAssertionError } if (process.env.NODE_ENV ! == 'production') { const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, unexpectedKeyCache ) if (warningMessage) { warning(warningMessage) } } let hasChanged = false const nextState = {} 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 errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey ! == previousStateForKey } hasChanged = hasChanged || finalReducerKeys.length ! == Object.keys(state).length return hasChanged ? nextState : state } }Copy the code
The combineReducers function combines multiple reducer functions into a composite reducer and returns a combined state. It can be seen from the previous dispatch of createStore that no differentiation was made during dispatch
First code
Get the reducers with the value of function clearly. If there is a Reducer with the value of undefined in the development environment, report warning
Second code
An assertReducerShape function is used
/ function assertReducerShape(reducers) {object.keys (reducers).foreach (key => { const reducer = reducers[key] const initialState = reducer(undefined, { type: ActionTypes.INIT }) if (typeof initialState === 'undefined') { throw new Error( `Reducer "${key}" returned undefined during initialization. ` + `If the state passed to the reducer is undefined, you must ` + `explicitly return the initial state. The initial state may ` + `not be undefined. If you don't want to set a value for this reducer, ` + `you can use null instead of undefined.` ) } if ( typeof reducer(undefined, { type: ActionTypes.PROBE_UNKNOWN_ACTION() }) === 'undefined' ) { throw new Error( `Reducer "${key}" returned undefined when probed with a random type. ` + `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` + `namespace. They are considered private. Instead, you must return the ` + `current state for any unknown actions, unless it is undefined, ` + `in which case you must return the initial state, regardless of the ` + `action type. The initial state may not be undefined, but can be null.` ) } }) }Copy the code
InitType and random type calls are used for each reducer. If undefined, warning is returned
Returns a combination function
The first is fault-tolerant code
The first one is that if the throw that we just threw is present, we’re going to get an error
The second is to determine whether state is a simple object and whether state has a key with reducers that does not exist
GetUnexpectedStateShapeWarningMessage function
function getUnexpectedStateShapeWarningMessage( inputState, reducers, action, unexpectedKeyCache ) { const reducerKeys = Object.keys(reducers) const argumentName = action && action.type === ActionTypes.INIT ? 'preloadedState argument passed to createStore' : 'Previous State received by the reducer' if (reducerkeys. length === 0) {return (' there is no valid reducer in the store Make sure that the argument passed to combineReducers is an object with a reducer value ')} // Determine whether state is a simple object if (! isPlainObject(inputState)) { return ( `The ${argumentName} has unexpected type of "` + {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + `". Expected argument to be an object with the following ` + `keys: "${reducerKeys.join('", "')}" ')} /* Check whether state unexpectedKeys */ const unexpectedKeys = object.keys (inputState).filter(key =>! reducers.hasOwnProperty(key) && ! unexpectedKeyCache[key] ) unexpectedKeys.forEach(key => { unexpectedKeyCache[key] = true }) if (action && action.type === ActionTypes.REPLACE) return if (unexpectedKeys.length > 0) { return ( `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` + `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` + `Expected to find one of the known reducer keys instead: ` + `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.` ) } }Copy the code
I’m not going to say it here but the core is these two lines of code
const unexpectedKeys = Object.keys(inputState).filter( key => ! reducers.hasOwnProperty(key) && ! unexpectedKeyCache[key] )Copy the code
All that remains is the Reducer trigger logic
// Initialize change to false let hasChanged = false const nextState = {} // loop reducers key for (let I = 0; i < finalReducerKeys.length; I ++) {const reducerKeys = finalReducerKeys[I] const Reducer = finalReducers[key] // Get the old state const previousStateForKey = State [key] // New state const nextStateForKey = Reducer (previousStateForKey, // Error if (typeof nextStateForKey === 'undefined') {const errorMessage = getUndefinedStateErrorMessage(key, Action) throw new Error(errorMessage)} // Merge state nextState[key] = nextStateForKey // If olderState is not equal to newState The change to true hasChanged = hasChanged | | nextStateForKey! = = previousStateForKey} / / if the state has not changed But state and reducers key is changed to change hasChanged = hasChanged | | finalReducerKeys.length ! == object.keys (state).length // Return state hasChanged? nextState : stateCopy the code
In addition to these mandatory apis, there is another function that is commonly used
bindActionCreators.js
The most common use of this method is with the connect second parameter of React-redux, mapDispatchToProps
Below is his source, in fact, is to do a layer of conversion processing
export default function bindActionCreators(actionCreators, If (Typeof actionCreator === 'function') {return BindActionCreator (actionCreators, Dispatch)} // If this is not a function and object or null then error if (Typeof actionCreators! == 'object' || actionCreators === null) { throw new Error( `bindActionCreators expected an object or a function, instead received ${ actionCreators === null ? 'null' : typeof actionCreators }. ` + `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"? ')} // Iterate over the conversion const boundActionCreators = {} for (const key in actionCreators) {const actionCreator = actionCreators[key] if (typeof actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, Dispatch)}} return boundActionCreators} // Returns a function closure that saves the Dispatch and actionCreator Function bindActionCreator(actionCreator, dispatch) { return function() { return dispatch(actionCreator.apply(this, arguments)) } }Copy the code
This is the end of the redux source code
The next step is to analyze the remaining issues of middleware implementation mechanism and operation process
Realization and operation flow of Redux middleware
It’s these lines of code that are the most confusing
At this point, we can combine the source code of redux-Thunk to interpret it
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => {
return next=> {
return action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
}
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
Copy the code
That’s all there is to redux-Thunk
So let’s first analyze what we can understand
1. Middleware was called during map, and an object with getState and Dispatch was passed in. After initialization, the Dispatch object was re-assigned to the enhanced Dispatch
So you can imagine
The dispatch here is the dispatch above
And then we look down
dispatch = compose(... chain)(store.dispatch)Copy the code
So let’s split this line of code like this
dispatch = compose(... chain) dispatch = dispatch(store.dispatch)Copy the code
So let’s look at compose (0)
export default function compose(... funcs) { if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (... args) => a(b(... args))) }Copy the code
Let’s start with the simple one. If the number of middleware is 1, we’ll just return that middleware. For example, redux-thunk
The part that is redlined (the upper parameters dispatch,getState are saved due to the closure)
Take down
dispatch = dispatch(store.dispatch)
Copy the code
The redlined part of the call is passed to the Store’s Dispatch (the original dispatch)
So that’s what I’m going to return
Then assign the return value to Dispatch (enhanced version)
So the thunk function looks like this
Now the remaining two lines of code are fairly understandable and the process for triggering dispatch should look something like this
Validation: So if you pass in a function with redux-thunk, you should call redux-thunk twice
Let’s take a look at multiple middleware
Here I simulate the situation of multiple middleware
const a = (store) =>{ console.log('a:', store) return (next)=> { console.log('a:',next) return (action) => { console.log('a:',action) return next(action) } } } const b = (store) =>{ console.log('b:', store) return (next)=> { console.log('b:',next) return (action) => { console.log('b:',action) return next(action) } } } const composeFunc = (... funs) => { return funs.reduce((a, b) => { return (... args) => a(b(... args)) }) } const middleWare = [a, b].map(item => item('store')) console.log(composeFunc(a, b),'---composeFunc(a, b)') const reinforce = composeFunc(a, b)('store.dispatch'); console.log(reinforce.toString())Copy the code
In this case, the final dispatch is
And now the next of function A is
Next of function B is the original dispatch
The process goes something like this
At this point Redux’s analysis ends up not being particularly difficult, but the design idea is pretty cheesy, especially the middleware design