This article is Redux source code analysis video text version, if you want to see me a sentence code analysis video version, you can pay attention to the end of the public number. I post videos every weekend, and I post articles every day that I think are good.

preface

Sync updates on my Github

Before we get down to business, let’s take a look at how Redux is used in a project and walk through the source code using the steps. Take my open source React project for example.

// First combine a reducer with combineReducers
const appReducer = combineReducers({
  user: UserReducer,
  goods: GoodsReducer,
  order: OrdersReducer,
  chat: ChatReducer
});
// appReducer is passed to createStore and thunkMiddleware is used through applyMiddleware
// replaceReducer implements hot update replacement
// Then dispatch(action) where needed to cause the state to change
export default function configureStore() {
  const store = createStore(
    rootReducer,
    compose(
      applyMiddleware(thunkMiddleware),
      window.devToolsExtension ? window.devToolsExtension() : f= > f
    )
  );

  if (module.hot) {
    module.hot.accept(".. /reducers", () = > {const nextRootReducer = require(".. /reducers/index");
      store.replaceReducer(nextRootReducer);
    });
  }

  return store;
}
Copy the code

After introducing the steps, let’s move on to the main topic.

The source code parsing

First let’s look at the combineReducers function

// Pass an object
export default function combineReducers(reducers) {
 // Get the key of this Object
  const reducerKeys = Object.keys(reducers)
  // Filtered reducers
  const finalReducers = {}
  // Get the value of each key
  // Determine whether the value is undefined in the development environment
  // Then put the values whose value types are functions into 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]
    }
  }
  // Get the key of the filtered reducers
  const finalReducerKeys = Object.keys(finalReducers)
  
  // In a development environment, save the cache of unexpected keys for the following warning
  let unexpectedKeyCache
  if(process.env.NODE_ENV ! = ='production') {
    unexpectedKeyCache = {}
  }
    
  let shapeAssertionError
  try {
  // This function is parsed below
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }
The combineReducers function returns a function, which is the merged reducer function
// This function returns the total state
// And you can also see that closures are used here, using some external properties inside the function
  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }
    // This function is parsed below
    if(process.env.NODE_ENV ! = ='production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }
    // State is changed
    let hasChanged = false
    // Change the state
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
    // Get the corresponding key
      const key = finalReducerKeys[i]
      // Obtain the reducer function corresponding to the key
      const reducer = finalReducers[key]
      // The key under the state tree is the same as the key under finalReducers
      // So the keys of the parameters you pass in the combineReducers represent both the reducer and the state
      const previousStateForKey = state[key]
      // Then run the reducer function to obtain the state corresponding to the key
      const nextStateForKey = reducer(previousStateForKey, action)
      // Check the value of state, undefined error
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      // Then insert value
      nextState[key] = nextStateForKey
      // If state changeshasChanged = hasChanged || nextStateForKey ! == previousStateForKey }// return the new state as long as it has been changed
    return hasChanged ? nextState : state
  }
}
Copy the code

The combineReducers function is generally very simple, summed up is to receive an object, filter the parameters and return a function. This function has an object finalReducers after filtering parameters, traverses this object, then executes each reducer function in the object, and finally returns the new state.

Let’s look at two functions used in combinrReducers

// This is the first error-throwing function executed
function assertReducerShape(reducers) {
// Iterate over the parameters in combineReducers
  Object.keys(reducers).forEach(key= > {
    const reducer = reducers[key]
    // Pass him an action
    const initialState = reducer(undefined, { type: ActionTypes.INIT })
    // If state is undefined, throw an error
    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.`)}// Filter again, in case you return actiontypes.init from the reducer
    // Pass a random action to determine if the value is 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.`)}}}function getUnexpectedStateShapeWarningMessage(inputState, reducers, action, unexpectedKeyCache) {
  // Reducers here are already finalReducers
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? 'preloadedState argument passed to createStore'
      : 'previous state received by the reducer'
  
  // If finalReducers is empty
  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 the state you pass is not an 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('",")}"`)}// Compare the included state with the keys under finalReducers to filter out the extra keys
  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 has value
  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

Let’s first look at the compose function

// This function is cleverly designed to allow us to nest multiple functions by passing in function references. The term is called higher-order functions
// Call the function from right to left using the reduce function
// For the example in the above project
compose(
    applyMiddleware(thunkMiddleware),
    window.devToolsExtension ? window.devToolsExtension() : f= > f
) 
// Now applyMiddleware(thunkMiddleware)(windod.devToolsextension ()())
// So you should return a function if window.devToolsextension is not found
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

We then parse part of the createStore function

export default function createStore(reducer, preloadedState, enhancer) {
  // If the second argument is a function and there is no third argument, switch the position
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
  // Check whether enhancer is a function
  if (typeofenhancer ! = ='undefined') {
    if (typeofenhancer ! = ='function') {
      throw new Error('Expected the enhancer to be a function.')}// If the type is correct, enhancer first and then createStore
    return enhancer(createStore)(reducer, preloadedState)
  }
  Check whether reducer is a function
  if (typeofreducer ! = ='function') {
    throw new Error('Expected the reducer to be a function.')}/ / the current reducer
  let currentReducer = reducer
  // Current status
  let currentState = preloadedState
  // Array of current listening functions
  let currentListeners = []
  // This is an important design to ensure that the currentListeners array does not change each time the listeners are iterated
  // We can think of currentListeners only, if I subscribe again in a subscribe
  // or unsubscribe, which causes the currentListeners array size to change, and may result in
  // Index error
  let nextListeners = currentListeners
  // Whether reducer is running
  let isDispatching = false
  // If currentListeners and nextListeners are the same, assign them back
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }
  / /...
}
Copy the code

Let’s start with the applyMiddleware function

Before I do that, I need to introduce currization of a function. Currization is the technique of converting a function that takes multiple arguments into a series of functions that take one argument.

function add(a,b) { return a + b }   
add(1.2) = >3
// The above function can be modified by using the Currization
function add(a) {
    return b= > {
        return a + b
    }
}
add(1) (2) = >3
// You can think of function corrification as saving an external variable through a closure, then returning a function that takes an argument, uses the saved variable in the function, and then returns the value.
Copy the code
// This function is probably the most difficult to understand in the entire source code
// This function returns a Currified function
// So calling this function should say applyMiddleware(... middlewares)(createStore)(... args)
export default function applyMiddleware(. middlewares) {
  return createStore= >(... args) => {// The createStore function is called to pass in the last call to applyMiddleware
    conststore = createStore(... args)let dispatch = (a)= > {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`)}let chain = []
    // Every middleware should have these two functions
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (. args) = >dispatch(... args) }// Pass each middleware in middlewares into the middlewareAPI
    chain = middlewares.map(middleware= > middleware(middlewareAPI))
    // As before, call each middleware from right to left and pass in store.dispatchdispatch = compose(... chain)(store.dispatch)// Redux-thunk (); // Redux-thunk ()
    CreateThunkMiddleware returns three layers of functions, the first of which takes middlewareAPI arguments
    // The second function receives store.dispatch
    // The third level function accepts the arguments in dispatch
{function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) = > next => action= > {
  // Determine whether the arguments in dispatch are functions
    if (typeof action === 'function') {
    // If the action is a function, pass these parameters in until the action is not a function and executes dispatch({tYEP: 'XXX'}).
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}
const thunk = createThunkMiddleware();

export defaultthunk; }// Finally return the property of the middleware enhanced dispatch in the remaining store, so that your dispatch
    return {
      ...store,
      dispatch
    }
  }
}
Copy the code

Ok, now that we’ve cracked the hard part, let’s look at some simple code

// Reduce the reducer with the current state
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
}
// Take a function argument
function subscribe(listener) {
    if (typeoflistener ! = ='function') {
      throw new Error('Expected listener to be a function.')}// The most important design process has already been explained, but there are few other important notes to note
    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 http://redux.js.org/docs/api/Store.html#subscribe for more details.')}let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

// Returns an unsubscribe function
    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 http://redux.js.org/docs/api/Store.html#subscribe for more details.'
        )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)}}function dispatch(action) {
// The native dispatch determines whether an action is an object
    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? ')}// Note that you cannot execute the dispatch function in Reducers
// Because if you execute dispatch in the Reducer function, an infinite loop will occur
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')}// Execute the combineReducers function
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
// Then iterate over the currentListeners, executing the functions saved in the array
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }
 // An action dispatch({type: actiontypes.init}) is then initiated at the end of the createStore;
 // To initialize state
Copy the code

At the end

If you have any more questions about Redux, leave a comment below. This article is a text version of the video, which was uploaded on January 14.

You can follow my official account, where I will update videos and post a good article every night.