Redux: one hundred lines of code and one thousand lines of document

Time:2021-10-7

It was only half a year since I came into contact with redux. I was confused about the official documents at the beginning and gradually understood what Redux was doing. However, in most scenarios, Redux was used together with react, so the react Redux library was introduced. However, it was precisely because the react Redux library encapsulated a large number of methods that our understanding of Redux became blurred. This article will analyze Redux from the perspective of Redux source code. I hope you have some foundation of Redux before reading.

Redux: one hundred lines of code and one thousand lines of document

The above figure is the flow chart of redux. The details are not introduced. Students who do not understand can refer to the official documents of redux. Written in great detail. The following code structure is the master branch of Redux:

├── applyMiddleware.js
├── bindActionCreators.js
├── combineReducers.js
├── compose.js
├── createStore.js
├── index.js
└── utils

└── warning.js

The directory under the SRC folder in Redux is as shown above. The file name basically corresponds to the familiar Redux API. First, take a look at the code in index.js:


/*
* This is a dummy function to check if the function name has been altered by minification.
* If the function has been minified and NODE_ENV !== 'production', warn the user.
*/
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 DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' +
    'to ensure you have the correct code for your production build.'
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose
}

The above code is very simple. It just exports all methods. amongisCrushedIs used to check whether the function name has been compressed (minification). If the function is not currently in the production environment and the function name is compressed, the user is prompted. Process is a global variable of the node application, which can obtain some information of the current process. Process. Env. Node is often used in many front-end libraries_ Env is an environment variable to judge whether it is currently in the development environment or production environment. In this small example, we can get a hack method. What if it is compressed when judging the name of a JS function? We can predefine a virtual function (although there is no virtual function in JavaScript, the dummy function here refers to a function without a function body), and then judge whether the function name during execution is the same as the predefined one, just like the above code:

function isCrushed() {}
if(typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed'){
    //has minified
}

compose

  From easy to difficult, we are looking at a slightly simpler external methodcompose


/**
 * Composes single-argument functions from right to left. The rightmost
 * function can take multiple arguments as it provides the signature for
 * the resulting composite function.
 *
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 * from right to left. For example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */

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)))
}

  Before we understand this function, let’s take a look at it firstreduceMethod, I’ve seen this method many times and still have a vague impression, although I’ve introduced it beforereduce, but remember againArray.prototye.reduce:

The reduce() method applies a function against an accumulator and each element in the array (from left to right) to reduce it to a single value.

   reduce()The function applies a function to an accumulated value and each element in the array (from left to right) toreduceAs a single value, for example:


var sum = [0, 1, 2, 3].reduce(function(acc, val) {
  return acc + val;
}, 0);
// sum is 6

   reduce()The callback function accepts two parameters: a callback function and an initial value. The callback function will be applied to each element of the array from left to right. The definition of the callback function is

/**
 *Accumulator: the accumulator accumulates the callback value, which is the cumulative value or initial value returned when the callback was called last time
 *Currentvalue: the value of the current array traversal
 *Currentindex: the index value of the current element
 *Array: entire array
 */
function (accumulator,currentValue,currentIndex,array){
    
}

Now look backcomposeWhat functions are doing,composeFunction combines multiple single parameter functions from left to right. The rightmost function can accept multiple parameters according to the definition, ifcomposeIf the parameter of is null, an empty function is returned. If the parameter length is 1, the function itself is returned. If the parameter of the function is an array, we return


  return funcs.reduce((a, b) => (...args) => a(b(...args)))

We knowreduceThe function returns a value. The callback function passed in by the above function is(a, b) => (...args) => a(b(...args))amongaIs the current cumulative value,bIs the value currently traversed in the array. Suppose you call the functioncompose(f,g,h)First, when the callback function is executed for the first time,aThe argument to is a functionf,bThe argument to isg, the second call is,aThe argument to is(...args) => f(g(...args)),bThe argument to ishFinally, the function returns(...args) =>x(h(...args))Where x is(...args) => f(g(...args))So we can finally deduce the runcompose(f,g,h)The result is(...args) => f(g(h(...args)))。 Did you find it? It’s actually through herereduceRealizedreduceRightRight to left traversal function, but it makes the code relatively difficult to understand. In Redux version 1.0.1composeThe implementation is as follows:

export default function compose(...funcs) {
     return funcs.reduceRight((composed, f) => f(composed));
}

Does this seem easier to understandcomposeFunction.

bindActionCreators

  bindActionCreatorsIt is also a very common API in Redux, which mainly implementsActionCreatorAnddispatchFor binding, see the official explanation:

Turns an object whose values are action creators, into an object with the same keys, but with every action creator wrapped into a dispatch call so they may be invoked directly.

Translated asbindActionCreatorsSet the value toactionCreatorThe object of is converted to an object with the same key value, but eachactionCreatorWill bedispatchThe package is called, so it can be used directly. Without much to say, let’s see how it is realized:


import warning from './utils/warning'

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  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"?`
    )
  }

  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    } else {
      warning(`bindActionCreators expected a function actionCreator for key '${key}', instead received type '${typeof actionCreator}'.`)
    }
  }
  return boundActionCreators
}

For processing individualactionCreatorThe best way is

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

The code is also very simple. It just returns a new function, which will be calledactionCreatorThe returned pure objectdispatch。 And for functionsbindActionCreatorsFirst, I will judgeactionCreatorsIs it a function? If it is a function, it will be called directlybindActionCreator。 WhenactionCreatorsAn error is thrown when it is not an object. next:


  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    } else {
      warning(`bindActionCreators expected a function actionCreator for key '${key}', instead received type '${typeof actionCreator}'.`)
    }
  }
  return boundActionCreators

This code is also very simple. Even I think I can write it, just for objectsactionCreatorsAll values in the callbindActionCreator, and then return the new object. Congratulations, another file has been unlocked~

applyMiddleware

  applyMiddlewareIt is an important API of Redux middleware. This part of the code doesn’t need to be explained again. Students who haven’t seen it stamp hereRedux: middleware, why are you so hard, there is a detailed introduction.

createStore

  createStoreAs the core API of Redux, its function is to generate an application unique store. The signature of its function is:

function createStore(reducer, preloadedState, enhancer) {}

The first two parameters are very familiar,reducerIt’s handledreducerPure function,preloadedStateIs the initial state, andenhancerRelatively little use,enhancerIs a higher-order function used tocreateStoreEnhanced functionality. Let’s look at the source code:

The specific codes are as follows:

import isPlainObject from 'lodash/isPlainObject'
import $$observable from 'symbol-observable'

export const ActionTypes = {
  INIT: '@@redux/INIT'
}

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

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

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


  function getState() {
    return currentState
  }

  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      isSubscribed = false

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

  function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
        'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
        'Have you misspelled a constant?'
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    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
  }

  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.INIT })
  }

  function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeof observer !== 'object') {
          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
      }
    }
  }

  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

Let’s read it step by step:


  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

We found that if no parameters were passed inenhancer, andpreloadedStateIf the value of is another function,createStoreWill think you omittedpreloadedStateSo the second parameter isenhancer


  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

If you pass inenhancerBut it’s not a function type. Will throw an error. If passed inreducerIt is not a function and throws a related error. The next step iscreateStoreKey, initialization:


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

  currentReducerIs used to store the currentreducerFunction.currentStateIt is used to store the data in the current store. It is initialized as the defaultpreloadedState,currentListenersUsed to store the current listener. andisDispatchingUsed to determine whether it is currently in processdispatchPhase of. Then the function declares a series of functions, and finally returns:


{
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
}

Obviously, you can see that the function returned isstore。 For example, we can callstore.dispatch。 Let’s take a look at what each function is doing in turn.

dispatch


  function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
        'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
        'Have you misspelled a constant?'
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    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
  }

Let’s seedispathWhat did you do? First check the incomingactionWhether it is a pure object. If not, an exception will be thrown. Then test,actionExists intypeIf it does not exist, the corresponding error prompt will be given. Then judgeisDispatchingIs ittrue, mainly to preventreducerZhongzuodispatchOperation, if inreduderI did itdispatch, anddispatchIt will inevitably lead toreducerThe call of will cause an endless loop. Then we willisDispatchingSet astrue, call the currentreducerFunction and returns a newstateDepositcurrentState, andisDispatchingPut it back. Finally, call the listener in turnstoreChanges have taken place, but we have not put the newstorePass it to the listener as a parameter, because we know that the listener function can be uniquely obtained by callingstoreFunction ofstore.getState()Get the lateststore

getState


  function getState() {
    return currentState
  }

It’s too simple to experience by yourself.

replaceReducer


  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.INIT })
  }

  replaceReducerThe use of is relatively small, and the main users are hot updatesreducer

subscribe


  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      isSubscribed = false

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

  subscribeUsed to subscribestoreFunction of variation. First judge the incominglistenerWhether it is a function. And then calledensureCanMutateNextListeners,


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

Can seeensureCanMutateNextListenersUsed to judgenextListenersandcurrentListenersIs it exactly the same, if so(===), willnextListenersAssign ascurrentListenersCopy of (the same value, but not the same array), and then pass in the current listener functionnextListeners。 Last returnunsubscribeFunction to remove the current listener. It should be noted that,isSubscribedIt determines whether the current listener function is listening in the form of closure, so as to ensure that only the first call is madeunsubscribeIs effective. But why does it existnextListenersAnd?

First, you can add at any point in timelistener。 Whether it isdispatchAction, orstateWhen the value is changing. But it should be noted that in each calldispatchPreviously, the subscriber was just a snapshot. If it was inlistenersIf a subscription or unsubscribe occurs during the call, it will not take effect immediately in this notification, but in the next notification. So the process of adding isnextListenersInstead of adding subscribers directly tocurrentListeners。 Then at each calldispatchI always do:

const listeners = currentListeners = nextListeners

To synchronizecurrentListenersandnextListeners

### observable

This part does not belong to the content explained in this article. It mainly involves rxjs and asynchronous response action. In the future, I will explain it separately if I have a chance (mainly because I understand it myself).

## combineReducers

  combineReducersThe main function of is toreducerThe function is split into small onesreducerHandle them separately and see how they are implemented:


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 = Object.keys(finalReducers)

  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
    }
    return hasChanged ? nextState : state
  }
}

First, through aforLoop to traverse parametersreducers, assign the corresponding value to the attribute of the functionfinalReducers。 Then declare the variableunexpectedKeyCache, if in a non production environment, it is initialized to{}。 Then executeassertReducerShape(finalReducers), if an exception is thrown, the error information is stored in theshapeAssertionError。 Let’s have a lookshapeAssertionErrorWhat’s going on?


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.`
      )
    }

    const type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.')
    if (typeof reducer(undefined, { type }) === '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.`
      )
    }
  })
}

It can be seen thatassertReducerShapeThe main function is to judgereducersEach ofreducerstayactionby{ type: ActionTypes.INIT }If there is no initial value, an exception will be thrown. And will be rightreduerPerform a randomaction, if there is no return, an error will be thrown to tell you not to process private actions in redux. For unknown actions, the current stat should be returned. And the initial value cannot beundefinedBut it can benull

Then we sawcombineReducersReturned acombineReducersFunction:


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
    }
    return hasChanged ? nextState : state
}

staycombinationIn the function, we firstshapeAssertionErrorHandle possible exceptions in. Then, if it is in the development environment, it will be executedgetUnexpectedStateShapeWarningMessage, lookgetUnexpectedStateShapeWarningMessageHow is it defined:


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 (
      '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] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    )
  }

  const unexpectedKeys = Object.keys(inputState).filter(key =>
    !reducers.hasOwnProperty(key) &&
    !unexpectedKeyCache[key]
  )

  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  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.`
    )
  }
}

Let’s take a brief lookgetUnexpectedStateShapeWarningMessageWhat issues have been addressed:

  1. Is there a reducer in the reducer

  2. Is state a pure object object

  3. There are items in state that are not handled by reducer, but they will be ignored only after the first reminder.

thencombinationExecute its core code:


    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
    }
    return hasChanged ? nextState : state

Use variablesnextStateRecord this executionreducerThe returned state.hasChangedUsed to record before and afterstateWhether it has changed. Loop traversalreducers, the correspondingstoreGive the part to the relevantreducerProcessing, of course, corresponding to eachreducerReturned NEWstateStill notundefined。 Final basishasChangedWhether to change to decide to returnnextStatestillstateThis ensures that the same object is still returned under the same condition.

Finally, in fact, we found that the source code of Redux is very refined and not complex, but it is not easy for Dan Abramov to evolve from the thought of flux to the current thought of redux. I hope this article will make you have a deeper understanding of redux.