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