Write it up front

This article is intended for readers who are experienced in using Redux, familiar with the use of Redux, and want to know what redux really is.

  • Engineering structure foundation
  • Redux (React-redux) basic usage

This will help you understand more quickly.

What is the story

Redux is an application state management tool. Its workflow can be seen in the following figure:

Redux updates the entire state tree after the Middleware and Reducer processes through user-triggered (dispatch) actions to update the view. That’s how Redux works, but let’s take a closer look at what’s going on.

Starting from the index

Find the root cause

First let’s open redux’s Github repository and look at the directory structure for the entire project:

. + -. Making/ISSUE_TEMPLATE / / lot issue template | + -- Bug_report. Md / / bug report template | + -- Custom. Md / / generic template + - build | + -- Gitbooks.css +-- examples // examples of how to use redux Detailed + - this article is not a logo / / redux static resource directory + logo - the core content of SRC / / redux directory | + -- utils / / redux of library core tools | | + -- actionTypes. Js / / Some default random actionTypes | | + -- isPlainObject. Js / / judge whether literal variable or a new object | | + -- warning. Js / / print warning tools | + -- ApplyMiddleware. Js / / | + -- mysterious magicbindActionCreator. Js mysterious magic | + -- combineReducers. / / js / / mysterious magic | + -- compose. Js mysterious magic | + -- createStore. / / js / / | mysterious magic +-- index.js // mysterious magic +--test// redux test case +--.bablerc.js // bable compiler configuration +--.editorConfig // editor configuration, --.eslintignore // esLint ignores file directory declarations +--.eslintrc.js // ESLint checks configuration +--.gitbook.yaml // Gitbook builds configuration --.gitignore // git submits ignored file directory declaration +--.prettierrc.json // the prettier code automatically reformats the configuration +--.travis. Yml // Travis CI configuration tool +-- Index.d.ts // Redux typescript variable declarations +-- package.json // NPM command and package management +-- rollup.config.js // rollup package compiler configurationCopy the code

Of course, the redux project directory also contains a number of MD files, which we won’t go into too much detail. Instead, we’ll focus on the roots of Redux, so let’s start with package.json:

"scripts": {
    "clean": "rimraf lib dist es coverage"."format": "prettier --write \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\""."format:check": "prettier --list-different \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\""."lint": "eslint src test"."pretest": "npm run build"."test": "jest"."test:watch": "npm test -- --watch"."test:cov": "npm test -- --coverage"."build": "rollup -c"."prepare": "npm run clean && npm run format:check && npm run lint && npm test"."examples:lint": "eslint examples"."examples:test": "cross-env CI=true babel-node examples/testAll.js"
},
Copy the code

From package.json we can find its NPM command configuration, Redux’s build command is packaged and compiled using rollup, so we can turn to the rollup configuration file rollup.config.js to find the roots of Redux. By reading the config file, we can find the following code:

{
    input: 'src/index.js'// output: {file:'lib/redux.js', format: 'cjs', indent: false },
    external: [
      ...Object.keys(pkg.dependencies || {}),
      ...Object.keys(pkg.peerDependencies || {})
    ],
    plugins: [babel()]
},
Copy the code

SRC /index.js is the entry point for the whole project. Here is the root of the project: SRC /index.js

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'
Copy the code

First, the dependency section of index uses the modules createStore, combineReducers, bindActionCreators, applyMiddleware, and compose in the same directory. The utility modules warning and __DO_NOT_USE__ActionTypes in the utils folder are also introduced. These two utility classes are obviously used for printing warnings and declaring unusable default actionTypes. Here’s what our index does:

function isCrushed() {}

if( process.env.NODE_ENV ! = ='production' &&
  typeof isCrushed.name === 'string'&& isCrushed.name ! = ='isCrushed'
) {
  warning(
    ...
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}
Copy the code

The name of the function will be crushed during production construction, and the name of the function will not be isCrushed. However, your environment is not production, which means you are running redux in production in dev, and redux will tell you if this happens. Next comes the time for export, and we will see, This includes previously introduced createStore, combineReducers, bindActionCreators, applyMiddleware, compose, and __DO_NOT_USE__ActionTypes. These are some of the apis and constants that we often use when working with Redux. Let’s go back to the source, one by one.

createStore

First, let’s look at our method for declaring a Redux store, createStore. As you know, we use this every time we initialize a Redux store:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers'; // In reducers, we used combinedReducer to merge multiple reducers into one reducerexport// Use thunk middleware to make dispatch accept functions for asynchronous operations, which I won't go into too much detail hereexport default createStore(rootReducer, applyMiddleware(thunk));
Copy the code

So how does createStore work? Let’s find the createStore function first

export default function createStore(reducer, preloadedState, enhancer) {
...
}
Copy the code

Accept parameters

Let’s start with its acceptance parameters:

  • reducerA function, you can do that by taking astate treeAnd return a new onestate tree
  • preloadedStateGenerated during initializationstate tree
  • enhancerA forreduxProvides enhanced functionalityfunction

CreateStore before

At the top of the function, there is a long list of judgments:

if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
    throw new Error(
      '... ')}if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
}

if(typeof enhancer ! = ='undefined') {
    if(typeof enhancer ! = ='function') {
      throw new Error('... ')}return enhancer(createStore)(reducer, preloadedState)
}

if(typeof reducer ! = ='function') {
    throw new Error('... ')}Copy the code

From these judgments, we can see some small rules for createStore:

  • Second parameterpreloadedStateAnd the third parameterenhancerCannot both be function types
  • A fourth argument cannot exist and is of function type
  • Without declarationpreloadedStateYou can use it directlyenhancerInstead ofpreloadedStateIn this casepreloadedStateThe default isundefined
  • If there isenhancer, and it is a functioncreateStoreparametricenhancerHigher order functions for the originalcreateStateAfter processing and terminatingcreateStoreprocess
  • reducerMust be a function.

Once these rules were met, we started the createStore process.

Start createStore

let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
Copy the code

We can clearly see that reducer and preloadedState are stored in the variables of the current function. In addition, we also declare the current listening event queue, and a state value isDispatching used to identify the current dispatching.

Let’s skip functions used as tools in the source code and get straight to the point:

Before we start with the first subscribe method, let’s look at the dispatch that is used to trigger the subscribe listener:

functionDispatch (action) {// Action must be an objectif(! isPlainObject(action)) { throw new Error('Actions must be plain objects. ' +
          'Use custom middleware for async actions.'} // The action must have onetype
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant? '} // If we are dispatching, the dispatch operation is not performed.if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.'} // Set the peer state totrueAnd use Reducer to generate a new state tree. try { isDispatching =trueCurrentState = currentReducer(currentState, Action)} finally {// When the new state tree is obtained, set the state to false. IsDispatching =falseConst Listeners = (currentListeners = nextListeners)for (leti = 0; i < listeners.length; I ++) {const listener = listeners[I] listener()return action
}
Copy the code

The subscribe function is called when the status tree is updated: subscribe

functionSubscribe (listener) {// Listener must be a functionif(typeof listener ! = ='function') { throw new Error(...) } // Throw an error if you are in dispatchif (isDispatching) {
      throw new Error(
        ...
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    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

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }
Copy the code

EnsureCanMutateNextListeners here we will use a method, this method is used to do? Let’s look at the code:

function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
}
Copy the code

During the process of defining variables, we found that currentListeners were defined as [] and nextLiteners pointed to references to the currentListeners. If I change the nextListeners, I change the currentListeners as well, making it impossible to tell the difference between the currentListeners and the last listeners. And in order to distinguish the ensureCanMutateNextListeners step processing.

After this process, the currentListeners are always the nextListeners of the last time they were executed:

// Place the most recent listeners on a queue to run and run const listeners = (currentListeners = nextListeners)Copy the code

The currentListeners are updated only when they execute subscribe again to update the nextListeners and the currentListeners are updated by executing dispatch again. Therefore, we need to pay attention to:

  • inlistenerPerformed in theunsubscribeIt doesn’t take effect immediately, because every timedispatchFunctions that execute listening queues use queues that are executeddispatchwhennextListenersTake a snapshot of the queue you update in the function next timedispatchSo try to make sureunsubscribeandsubscribeindispatchThis ensures that the listener queue is up to date each time it is used.
  • inlistenerWhen executed, the state tree fetched directly may not be the latest state tree, because yourlistenerIt is not clear whether it was executed again during its executiondispatch(), so we need a method:
function getState() {
    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
}
Copy the code

To get the current true and complete state.

Subscribe, Dispatch, listener, subscribe, subscribe, Dispatch, subscribe

function replaceReducer(nextReducer) {
    if(typeof nextReducer ! = ='function') {
      throw new Error('... ')
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
}
Copy the code

This is a method thrown by REdux to replace the current reducer that is being executed in the entire REdux with the newly passed Reducer, and it fires a built-in replace event by default.

The final ripple (fog, in this case, provides an API reserved for interaction with observable/ Reactive libraries. Let’s look at the core of the API code:

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

OuterSubscribe is the outerSubscribe method exposed by Redux. When an external library subscribes using the subscribe method in the exposed object, it can always pass through its incoming observer. The current state is retrieved (via the next and getState methods on its observer object), and the class library method is placed on the Redux listener queue nextListeners, expecting that every time a Dispatch operation occurs, Notifies the observer of updates to the status tree, and finally returns a method to unsubscribe (the return value of the subscribe method is the method to unsubscribe the current subscription).

At this point, the createStore veil has finally been lifted, and we now know all of the createStore methods:

  • dispatchUsed to trigger action, passreducerwillstateupdate
  • subscribeTo subscribe todispatchWhen usingdispatchWill notify all subscribers and execute their internallistener
  • getStateUsed to get the currentreduxThe latest state tree in
  • replaceReducerUsed to link the currentreduxIn thereducerAnd it triggers the default built-inREPLACE action.
  • [?observable]([Symbol.observable])(See Symbol. Observable for those who don’t know ithere), which can be providedObservable/Reactive (Observer/Reactive programming)Class library to subscribereduxIn thedispatchThe way, wheneverdispatchWill be up to datestatePassed to the subscriptionAn observer.

conclusion

Read through the redux source code in the work off and on writing the first finally completed, through a method by method analysis, although there are many omissions, but the author is also from which to deepen the understanding of Redux, I hope this article can also give you some read the source code ideas and understanding of Redux.

Thank you very much for reading ~