I haven’t touched the React family bucket for nearly a year. Recently, a project team changed to use the React technology stack, so I reviewed it again recently. Pick up the old knowledge at the same time and have some new harvest, here composition to remember it.

You should know how to use Redux(not React-Redux) before reading the article.

1. Prepare the environment

In order to better understand the source code, we can copy the source code to the local, and then build a development environment. Redux doesn’t rely on React, so you can use it in a very simple JavaScript project. There is no need to describe the process of setting up the development environment. Students who need it can directly copy my code to the local, and then install the dependency and run the project.

$ git clone https://github.com/zhongdeming428/redux && cd redux

$ npm i

$ npm run dev
Copy the code

Second, read the source code

(1) Source code structure

Ignore the documentation in the project and just look at the SRC source directory, which has the following structure:

The SRC ├ ─ ─ applyMiddleware. Js// Application middleware API├ ─ ─ bindActionCreators. Js// Convert the actionCreators API├ ─ ─ combineReducers. Js// Combine the reducer API├ ─ ─ compose. Js// Utility functions for nested calls to middleware├ ─ ─ createStore. Js// create store API├ ─ ─ index. Js// Redux project entry file for unified exposure of all apis├── ├─ ├.js// The script I created for debugging└ ─ ─ utils// A directory for tool functions├ ─ ─ actionTypes. Js// Defines some action types reserved for redux├ ─ ─ isPlainObject. Js// Is used to determine whether the object is pure└ ─ ─ warning. Js// Use to throw appropriate warning messages

Copy the code

As you can see, the source structure of Redux is simple and clear, with several major (and only) apis scattered into a single file module as much as possible, and we just need to look at each one.

(2) index. Js

The previous section mentioned that index.js is the entry file for the Redux project and is used to expose all apis, so let’s look at the code:

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'
// Different apis are written in different js files, and finally exported through index.js.

// This function is used to determine whether the current code has been compressed by a packaging tool (such as Webpack), and if so,
// The name of isCrushed function will be replaced. If the function name is replaced but process.env.node_env does not equal production
// remind the user to use lean code in production.
function isCrushed() {}

if( process.env.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 the main API.
export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}
Copy the code

I removed all the English comments to make it shorter. If you want to see the original comments, you can go to the Redux project to see the source code.

You can see that all API modules and utility functions are introduced in the header of the application, and then exported uniformly at the bottom. This part is relatively simple, mainly isCrushed function is a bit interesting. The author uses this function to determine if the code has been compressed (if the function name has been replaced).

This section also references utility functions, but since these functions are relatively simple, you can take a look at what they do first.

(3) Utility functions

With the exception of the compose function, all utility functions are placed in the utils directory.

actionTypes.js

This tool module defines several redux reserved action types, including Reducer replacement type, Reducer initialization type, and random type. The following source code:

// Defines some action types reserved for Redux.
// Random strings ensure uniqueness.
const randomString = (a)= >
  Math.random()
    .toString(36)
    .substring(7)
    .split(' ')
    .join('. ')

const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`.REPLACE: `@@redux/REPLACE${randomString()}`.PROBE_UNKNOWN_ACTION: (a)= > `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}

export default ActionTypes
Copy the code

This returns an ActionTypes object containing three types: INIT, REPLACE, and PROBE_UNKNOW_ACTION. They correspond to the types mentioned above. In order to prevent conflicts with user-defined action types, random values are deliberately added into type. In later use, comparisons are made by introducing ActionType objects.

isPlainObject.js

This function is used to determine if the object passed is pure. Redux requires action and state to be pure objects, so this function was born.

On the source code:

/** * If a parameter is pure, the definition of a pure Object is that its constructor is Object. * For example: {name: 'isPlainObject', type: 'funciton'} * The isPlainObject Function is not a pure object because its constructor is Function. * @param {any} obj Specifies the object to check. * @returns {Boolean} Check results returned. True indicates pure objects. * /
export default function isPlainObject(obj) {
  if (typeofobj ! = ='object' || obj === null) return false

  let proto = obj
  // Get the top-level prototype, if it is itself, then it is a pure object.
  while (Object.getPrototypeOf(proto) ! = =null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}
Copy the code

warning.js

This function is used to throw appropriate warnings, nothing to say.

/** * Prints a warning in the console if it exists. * * @param {String} message The warning message. * @returns {void} * /
export default function warning(message) {
  /* eslint-disable no-console */
  if (typeof console! = ='undefined' && typeof console.error === 'function') {
    console.error(message)
  }
  /* eslint-enable no-console */
  try {
    // This error was thrown as a convenience so that if you enable
    // "break on all exceptions" in your console,
    // it would pause the execution at this line.
    throw new Error(message)
  } catch (e) {} // eslint-disable-line no-empty
}
Copy the code

compose.js

This function is used to nest calls to middleware for initialization.

/** * Passes in a series of single-argument functions as arguments (funcs array), returns a new function that can take * multiple arguments and calls funcs functions from right to left at run time. * @param {... Function} funcs A family of middleware. * @returns {Function} Returns the result Function. A right-to-left call, such as compose(f, g, h), will return a new function * (... args) => f(g(h(... args))). */
export default function compose(. funcs) {
  if (funcs.length === 0) {
    return arg= > arg
  }

  if (funcs.length === 1) {
    return funcs[0]}// Iterate through reduce.
  return funcs.reduce((a, b) = >(... args) => a(b(... args))) }Copy the code

(4) createStore. Js

After looking at utility functions and entry functions, it’s time to get into the main topic. An important step we took with Redux was to create a store through the createStore method. So let’s see how this method creates a store, and what is a store?

Let’s look at the source code:

import? observablefrom 'symbol-observable'
// More on that later.
import ActionTypes from './utils/actionTypes'
// Introduce some predefined reserved action types.
import isPlainObject from './utils/isPlainObject'
// Check whether an object is pure.

// The main API for using Redux is this createStore, which creates a Redux Store that provides you with state management.
// It accepts three parameters (the second three optional). The first is reducer, which changes the state of the redux store. The second is the initialized store,
// A snapshot of the initial store; The third parameter is the enhancer object returned by the applyMiddleware function, which is required to use middleware
// The parameters provided.
export default function createStore(reducer, preloadedState, enhancer) {
  // The following paragraphs can be used to modify the parameters.
  / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * parameter adaptation * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
  if((typeof preloadedState === 'function' && typeof enhancer === 'function') | | (typeof enhancer === 'function' && typeof arguments[3= = ='function')) {// If more than one enhancer is passed, an error is thrown.
    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')}// If the default state is not passed (preloadedState is the function type, enhancer is the undefined type), then it is passed
  // preloadedState = enhancer
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeofenhancer ! = ='undefined') {
    if (typeofenhancer ! = ='function') {
      // If enhancer is not null and not a function type, an error is reported.
      throw new Error('Expected the enhancer to be a function.')}// Use enhancer to process the createStore. Note that there is no passing of   enhancer  As a parameter. Enhancer actually handles createStore and returns an actual createStore to create a store object, see applymiddleware.js.
    return enhancer(createStore)(reducer, preloadedState)
  }
  // If reducer is not a function type, an error is reported.
  if (typeofreducer ! = ='function') {
    throw new Error('Expected the reducer to be a function.')}/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /

  // Define a series of local variables inside a function to store data.
  let currentReducer = reducer  // Save the current reducer.
  let currentState = preloadedState // Used to store the current store, i.e., state.
  let currentListeners = [] // Stores all current subscribers registered with store.subscribe.
  let nextListeners = currentListeners  // New Listeners array, make sure not to modify the listeners directly.
  let isDispatching = false // Current state, prevent reducer nested calls.

  // As the name suggests, ensure that the nextListeners can be modified if they point to the same array as the currentListeners
  // Make the nextListeners copies of the currentListeners. Prevent currentListeners from changing the nextListeners.
  // I wasn't sure why the nextListeners existed at first, because the dispatchfunction assigned nextListeners directly to currentListeners.
  CurrentListeners are also available. Later to redux repo searched search, found an issue (https://github.com/reduxjs/redux/issues/2157) about the reasons for this practice.
  / / submit the explanation of the author of this code (https://github.com/reduxjs/redux/commit/c031c0a8d900e0e95a4915ecc0f96c6fe2d6e92b) is to prevent the Array. The slice of abuse, The Array.slice method is called to copy listeners only if necessary.
  // The old practice was to slice once per dispatch, which resulted in performance degradation.
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // Return currentState, which is a snapshot of store.
  function getState() {
    Prevent this method from being called on the Reducer because the Reducer accepts the state parameter.
    if (isDispatching) {
      throw new Error(
        '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
  }

  // store. Subscribe function, subscribe to dispatch.
  function subscribe(listener) {
    if (typeoflistener ! = ='function') {
      throw new Error('Expected the listener to be a function.')}// Subscriptions are not allowed in the Reducer.
    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#subscribe(listener) for more details.')}let isSubscribed = true
    // Make sure you can modify the Process before each procedure.
    ensureCanMutateNextListeners()
    // Store the subscriber's registration method.
    nextListeners.push(listener)

    // Returns a function to unregister the current subscriber.
    return function unsubscribe() {
      if(! isSubscribed) {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#subscribe(listener) for more details.'
        )
      }

      isSubscribed = false
      // Make sure you can modify the Process before each procedure.
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)}}// the store. Dispatch function is used to trigger the reducer to modify state.
  function dispatch(action) {
    if(! isPlainObject(action)) {// Actions must be pure objects.
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.')}if (typeof action.type === 'undefined') {
      // Each action must contain a type attribute that specifies the type to be modified.
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant? ')}No actions are allowed to be sent internally.
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')}try {
      Before calling reducer, mark position 1.
      isDispatching = true
      Call reducer, and the return value is the latest state.
      currentState = currentReducer(currentState, action)
    } finally {
      // After the call, position 0 will be marked, indicating the end of dispatch.
      isDispatching = false
    }

    // Execute all subscriber functions after dispatch.
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    // Return the action currently in use. This step is the key to middleware nesting.
    return action
  }

  // A relatively new API for dynamically replacing the current reducers. Suitable for loading on demand, code splitting and other scenarios.
  function replaceReducer(nextReducer) {
    if (typeofnextReducer ! = ='function') {
      throw new Error('Expected the nextReducer to be a function.')}// Execute the default REPLACE action. This type is used in combineReducers functions.
    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

  / / this is for the sake of ECMA TC39 meeting of a relevant observables proposal (see https://github.com/tc39/proposal-observable) written by a function.
  // Subscribes to store changes. Applies to all Observable libraries (mainly RxJS).
  / / I found the introduction of the function of the commit:https://github.com/reduxjs/redux/pull/1632.
  function observable() {
    OuterSubscribe is the external subscribe function.
    const outerSubscribe = subscribe
    // Return a pure object containing the SUBSCRIBE method.
    return {
      subscribe(observer) {
        if (typeofobserver ! = ='object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')}// A function that registers a subscribe, strictly Observable, observer must have a next attribute.
        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      / /? Observable (Symbol. Observable) is also an Observable specification that returns itself.
      [?observable]() {
        return this}}}// Dispatch an action of type INIT during initialization to verify various cases.
  dispatch({ type: ActionTypes.INIT })

  // Return a store object.
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [?observable]: observable
  }
}
Copy the code

As you can see, our Store object is a pure JavaScript object. Contains several property apis, and our state is stored inside the createStore method as a local variable that can only be accessed through the getState method. This is actually a closure exploit, where all the states we operate on are stored in a variable inside the getState method. Until our program ends (or the store is destroyed), the createStore method is reclaimed and its variables are destroyed.

The Subscribe method exploits the observer mode. We register our function with subscribe, and our function is stored in a local variable in the createStore method. CurrentListeners are iterated through each time dispatch is invoked, executing the methods in turn to meet our subscription requirements.

(5) combineReducers. Js

Create Reducer combineReducers combineReducers Reducer reducer

When we write reducer, we are actually writing a series of functions, and then the whole of the attributes of an object, and finally passed to combineReducers for processing, after processing, it can be used by createStore.

Such as:

// Create our reducers.
const _reducers = {
  items(items = [], { type, payload }) {
    if (type === 'ADD_ITEMS') items.push(payload);
    return items;
  },
  isLoading(isLoading = false, { type, payload }) {
    if (type === 'IS_LOADING') return true;
    return false; }};// Pass to combineReducers for createStore.
const reducers = combineReducers(_reducers);
// createStore accepts reducers to create the store we need.
const store = createStore(reducers);
Copy the code

So what does combineReducers do to our reducers objects?

This is a long code, so I hope you’ll be patient.

import ActionTypes from './utils/actionTypes'
import warning from './utils/warning'
import isPlainObject from './utils/isPlainObject'

If a reducer call you defined returns undefined, call this reducer to throw the appropriate error message. * * @param {String} key Specifies a reducer function name that you defined, which is also an attribute name of state. * * @param {Object} Action Action used when the reducer was called. * /
function getUndefinedStateErrorMessage(key, action) {
  const actionType = action && action.type
  const actionDescription =
    (actionType && `action "The ${String(actionType)}"`) | |'an action'

  return (
    `Given ${actionDescription}, reducer "${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.`)}/** * tool function, used to verify unknown keys. If a reducer does not correspond to an attribute in state, an error message is returned. * For action types of REPLACE, no verification is performed. * @param {Object} inputState * @param {Object} reducers * @param {Object} action * @param {Object} unexpectedKeyCache */
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 the reducers length is 0, an error message is returned.
  if (reducerKeys.length === 0) {
    return (
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.')}if(! isPlainObject(inputState)) {return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/) [1] +             // {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1]
      `". Expected argument to be an object with the following ` +          // Returns the type of inputState.
      `keys: "${reducerKeys.join('",")}"`)}// fetch all attributes that State has but reducers do not add to unexpectedKeysCache.
  const unexpectedKeys = Object.keys(inputState).filter(
    key= >! reducers.hasOwnProperty(key) && ! unexpectedKeyCache[key] )// Add to unexpectedKeyCache.
  unexpectedKeys.forEach(key= > {
    unexpectedKeyCache[key] = true
  })

  // If the action type is REPLACE, unknown keys are no longer verified, because the reducers loaded on demand do not need to be verified, and the reducers that do not exist now may be added next time.
  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.`)}}/** * Verify that all reducer is reasonable: undefined is not returned if any value is passed. * @param {Object} reducers The reducers objects you defined. * /
function assertReducerShape(reducers) {
  Object.keys(reducers).forEach(key= > {
    const reducer = reducers[key]
    // Get initialization state.
    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 the initialization checks pass, you may have defined actiontypes.init. At this point, the random value is checked again.
    // If undefined is returned, the user may have done something to INIT type, which is not allowed.
    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.`)}}}// Convert your defined reducers object into a large summary function.
// As you can see, combineReducers takes a reducers object as a parameter,
// Then return a total function as the final legitimate reducer, this reducer
// Take action as an argument and traverse all reducer calls based on the action type.
export default function combineReducers(reducers) {
  // Get all the attribute names of reducers.
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  // Run through all attributes of the reducers and eliminate all invalid reducers.
  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') {
      // Copy all reducers into the new finalReducers object.
      finalReducers[key] = reducers[key]
    }
  }
  // finalReducers is a pure filtered reducers, refetch all attribute names.
  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  // unexpectedKeyCache contains all attributes that are present in state but not in reducers.
  if(process.env.NODE_ENV ! = ='production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError
  try {
    // Check all reducer rationality, cache error.
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  This is the new reducer returned, a pure function. Execute the combination function each time you dispatch an action,
  // Then do all the reducer you have defined.
  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      // If there are cache errors, throw.
      throw shapeAssertionError
    }

    if(process.env.NODE_ENV ! = ='production') {
      // Check whether all the attributes in state have corresponding reducer in non-production environments.
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    let hasChanged = false
    // state Indicates whether to change the flag bit.
    const nextState = {}
    New state returned by reducer.
    for (let i = 0; i < finalReducerKeys.length; i++) {   // Traverse all reducer files.
      const key = finalReducerKeys[i]  // Obtain the reducer name.
      const reducer = finalReducers[key]  // Obtain the reducer.
      const previousStateForKey = state[key]  // The old state value.
      const nextStateForKey = reducer(previousStateForKey, action)  // Execute the new state[key] value returned by the reducer.
      if (typeof nextStateForKey === 'undefined') {
        If the reducer returns undefined after all the validation, then the reducer should throw an error message.
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      Add the reducer name to the nextState object. The reducer name you defined is an attribute of the corresponding state.hasChanged = hasChanged || nextStateForKey ! == previousStateForKey// Check whether the state has changed.
    }
    // Return the corresponding state according to the flag bit.
    return hasChanged ? nextState : state
  }
}
Copy the code

(6) applyMiddleware. Js

The combineReducers method has a lot of code, but the actual logic is very simple. The following function is not much code, but the logic is a little more complicated. It is the applyMiddleware function of application middleware. The idea of this function is clever and worth learning.

import compose from './compose'

//  A function for application middleware that can pass multiple middleware at the same time. The standard form of middleware is:
//  const middleware = store => next => action => { /*.....*/ return next(action); }
export default function applyMiddleware(. middlewares) {
  //  Returns a function that accepts   createStore  As a parameter. The ARGS parameters are reducer and preloadedState.
  return createStore= >(... args) => {// Call createStore inside the function to create an   Store object, where enhancer is not passed, because applyMiddleware itself creates an enhancer and calls it to createStore.
    // This actually delays the creation of a store by applyMiddleware. Why delay? With middleWares to work with, you initialize the middleware, redefine the dispatch, and then create the Store, The dispatch method contained in the store created at this point is different from the dispatch method created when enhancer is not passed. It contains some logic defined by the middleware, which is why the middleware can interfere with the Dispatch.
    conststore = createStore(... args)// This redefines dispatch so that no matter what parameter is passed in, an error will be reported
    // Call dispatch.
    let dispatch = (a)= > {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`)}const middlewareAPI = {
      getState: store.getState,
      dispatch: (. args) = >dispatch(... args)// Note that the dispatch function is not accessed during the final dispatch because it is overridden by the following dispatches.
    }
    // For each piece of middleware, the middlewareAPI is called, and this is middleware initialization.
    // The initialized middleware returns a new function that takes store.dispatch as an argument and returns a replacement dispatch as a new one
    / / store. Dispatch.
    const chain = middlewares.map(middleware= > middleware(middlewareAPI))
    The compose method calls all middleware in tandem. Replace the dispatch function with the final result, and all the store.dispatch methods used thereafter are already
    // New logic is added.dispatch = compose(... chain)(store.dispatch)// After initializing the middleware, override the dispatch function that reported the error.

    /**
     * middle 的标准形式:
     * const middleware = ({ getState, dispatch }) => next => action => {
     *    // ....
     *    return next(action);
     * }
     * 这里 next 是经过上一个 middleware 处理了的 dispatch 方法。
     * next(action) 返回的仍然是一个 dispatch 方法。
     */

    return {
      ...store,
      dispatch  // New dispatch.}}}Copy the code

The amount of code is really small, but it’s really clever, and there are a few key points:

  • The compose method leveragesarray.prototype. reduce to implement a nested call to the middleware, returning an entirely new function that can accept new parameters (the dispatches handled by the previous middleware), and ultimately returning an entirely new Dispatch method with new logic. See the format of the middleware function that is returned after initialization:

    next => action= > {
      return next(action);
    }
    Copy the code

    Next can be thought of as dispatch, taking a Dispatch as an argument and returning a new Dispatch method. The reason is that we can assume that all functions that accept action as an argument and then trigger the Reducer change state are dispatch functions.

  • After being initialized, the middleware returns a new function that takes dispatch as an argument, and then returns a new Dispatch that can be called by the next middleware, making all middleware calls nested! The final result is also a dispatch function.

    The resulting dispatch method is passed the original store.dispatch method to the last middleware, which is then nested, and finally passed through the method returned by the first middleware. So when we call the dispatch function with middleware, all the parameters of the applyMiddleware method are processed from left to right. It’s a bit like wrapping and unwrapping.

  • Why do actions go through all the middleware processing? Let’s look at the basic structure of middleware:

    ({ dispatch, getState }) => next= > action => {
      return next(action);
    }
    Copy the code

    We can see that once the action enters the function, it is processed by Next and returns the result. What will next return? Since the first next value is store.dispatch, look at the source code for store.dispatch.

    function dispatch(action) {
      // omit a series of operation code......
    
      // Return the action currently in use. This step is key to middleware nesting.
      return action
    }
    Copy the code

    Yes, store.dispatch eventually returned an action, and because the middleware nested calls, each Next returned an action, which could then be used by the next, neatly interlocking.

This part of the description is a bit of a mouthful, language anxious but do not want to draw a picture, you still think about it.

(7) bindActionCreators. Js

Reducer reducer Reducer Reducer Reducer Reducer Reducer Reducer reducer reducer reducer reducer reducer

const addItems = item= > ({
  type: 'ADD_ITEMS'.payload: item
});
Copy the code

Then you call it:

store.dispatch(addItems('item value'));
Copy the code

If you use bindActionCreators:

const _addItems = bindActionCreators(addItems, store.dispatch);
Copy the code

When you need to dispatch reducer:

_addItems('item value');Copy the code

This reduces the amount of duplicate code you need to write, and you can also write all your actions in one object and pass it to bindActionCreators, just as you did with the Combiner Creators object.

Look at the source code below:

/** * This function returns a new function. Calling the new function directly dispatches the action returned by ActionCreator. * This function is the basis of the bindActionCreators function, which splits actionCreators into actionCreators. Then call the bindActionCreator method. * @param {Function} actionCreator a Function that returns the action pure object. * @param {Function} dispatch store.dispatch is used to trigger the reducer. * /
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this.arguments))}}//  Accept an actionCreator (or an   ActionCreators object) and an EMSP; dispatch  Function as a parameter,
// Then return a function or an object. Dispatch.
export default function bindActionCreators(actionCreators, dispatch) {
  // If actionCreators is a function rather than an object, the bindActionCreators method is called directly for the conversion, which returns
  // The result is also a function that dispatches the corresponding   The action.
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  //  actionCreators  Throws an error if it is neither a function nor an object, or if it is null.
  if (typeofactionCreators ! = ='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"? `)}//  If & emsp; If actionCreators is an object, each of its properties should be an EMSP. ActionCreator, walk through each   ActionCreator,
  //  Use bindActionCreator for the conversion.
  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    //  Bind the conversion result to   BoundActionCreators object, which is returned at the end.
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}
Copy the code

This part is pretty simple, but the main purpose is to turn Action Creator into a function that you can use directly.

3. Middleware

Read the source code, feel middleware and not imagined so obscure. It’s just a basic format, and then you can do whatever you want in your middleware, and then you call the fixed method, return the fixed content and you’re done. This is why the source code for most Redux Middleware is short and snappy.

Redux-thunk redux-Thunk

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) = > next => 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

Is it short and small? So what does it do to make it so popular?

In fact, redux-thunk can be thought of as:

// This is a typical middleware format.
({ dispatch, getState }) => next= > action => {
  // Next is the dispatch method. The function where the comment is located is the new dispatch that is returned.
  // Check if action is a function.
  if (typeof action === 'function') {
    // If it is a function, call it, passing dispatch, getState, and the extra arguments as arguments to aciton.
    return action(dispatch, getState, extraArgument);
  }
  // If the action is not a function, nextr calls the action directly and returns the result.
  return next(action);
};
Copy the code

How about that? Is that easy? All it does is determine the type of action, call it if it’s a function, call it with Dispatch if it’s not a function, easy enough.

But what it does is practical, allowing us to pass a function as an argument to store.dispatch, which should be fixed and must conform to the code above, taking dispatch and getState as arguments, and then returning the actual action.

We can also write our own middleware:

({ dispatch, getState }) => next= > action => {
  return action.then ? action.then(next) : next(action);
}
Copy the code

This middleware allows us to pass a Promise object as an action and wait for the action to return a result (an actual action) before we dispatch.

Of course, since action.then() does not return the actual action (a pure object), this middleware may not be able to be used with other middleware, otherwise the other middleware will have problems accepting the action. This is just an example to show that middleware is not that complex, but there are a lot of things we can do with middleware.

For more sophisticated Redux middleware, see:

  • redux-promise
  • redux-saga
  • redux-logger

Four,

  • Redux is small and elegant, using closures and observer patterns, then chains of responsibility, adapters, and so on to build a store empire. The Store has its own territory, and to obtain or change the contents of the store, it must be implemented through the store functions.
  • Redux has less code and less customization than Vuex, which results in lower ease of use but higher customizability. This also fits the Vue and React style.
  • Redux source code is easier to understand, read the source code is easier to master the use of Redux, do not be intimidated.
  • Redux middleware is short and practical. If learning middleware from usage is difficult, try learning middleware from source code.

Finally, the time is limited, the level is limited, inevitably there are mistakes or mistakes, please forgive, give advice, common progress.

Welcome to my GitHub to download the project source; Or Follow me.