background

As redux is one of the brightest lights in front-end management, it’s important to understand how he works. This article takes a fresh look at Redux from source code and practice. Pure dry goods share!!

The following will focus on the implementation principle of Redux and the core middleware principle

  1. The return value of createStore
  2. Enhancer and its implementation of applyMiddleware
  3. Compose function and middleware principle and writing
  4. Clever aspects of Dispatch design and middleware
 export const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
);
Copy the code

We’ve seen this code a lot, and this article will walk you through the framework from the createStore entry point. The following source code is v3.7.2 version of the code.

createStore

First, look at the createStore function source code. A lot of extraneous code is omitted for easy comprehension and reading, so you can fold it up as you read.

export default function createStore(reducer, preloadedState, enhancer) {
   // If there are only two arguments, and the second argument is a function, it will be passed to enhancer
   if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
    
    // Omit a bunch of judgment logic
    return enhancer(createStore)(reducer, preloadedState)
  }
   
   // A bunch of method definitions
   dispatch({ type: ActionTypes.INIT });
   return {
    dispatch,  // Focus on
    subscribe, // Focus on
    getState, // Return the state method
    replaceReducer, // The reducer should be replaced dynamically when subcontracted
    [?observable]: observable
  }
}
Copy the code
  • You can see from the code that the return value of store is an object with multiple methods
  • Enhancer is a function extension. The return value is a store
function myEnhancer(createStore){
    return (reducer, preloadedState, enhancer) = > {
       // Do someSting before creating store
        const store = createStore(reducer, preloadedState, enhancer)
        //store something
        returnstore; }}Copy the code
  • Once the store is created, a default initial action is dispatched to initialize it. This step can be analogous to function self-execution in order for each reducer to return their default state to form the initial global state.
  • The global state is just a normal object function, and all other operations are used to help manage the state

dispatch

{type: ‘INCREACE’, payload: 1}}

  function dispatch(action) {
    // Check various acton types
    try {
      isDispatching = true
      CurrentState is the original state
      CurrentReducer is the reducer passed at the beginning of createStore
      currentState = currentReducer(currentState, action)
      // Reducer returns the updated state
    } finally {
      isDispatching = false
    }

    // Update the listener function
    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }
Copy the code
  • Dispatch triggers an action, executes the reducer, updates the listener, and returns the action itself. Why return action here? The answer is for chained composition of middleware, explained in detail in the middleware section.
  • The type check for an action requires that it be a normal object with the type attribute
  • Reducer is a function that receives two parameters, state and action, and returns a new state. State may be undefined during initialization, so the initial state of reducer is returned by triggering the default action. Reducer common formats are as follows:
function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          text: action.text,
          completed: false}]case 'COMPLETE_TODO':
      return state.map((todo, index) = > {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: true})}return todo
      })
    default:
      return state
  }
}
Copy the code
  • After the reducer execution was complete, state was updated, and the listeners were aware of the tools without any parameters. One of the tools is the react-Redux connect update mechanism, which is a high order component of react-Redux.

subscribe

Subscribe is a simple listener mode that collects listeners. The source code is very simple as follows

function subscribe(listener) {
    // Check the listener type
    let isSubscribed = true
    
    ensureCanMutateNextListeners() 
    // The function will copy a currentListeners
    // Ensure that other listeners are not affected during update
    nextListeners.push(listener)
    
    return function unsubscribe() {
      if(! isSubscribed) {return
      }
      // omit some error checks
      isSubscribed = false
    
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
      currentListeners = null
      // currentListeners will be retrieved from nextListeners the next time they run
      // The main purpose of this method is to prevent subscribe or unsubscribe errors during dispatch execution}}Copy the code

At this point, the core functionality of Redux has been covered, but the power of Redux has not been demonstrated. Next, we will introduce redux’s extended functionality middleware.

applyMiddleware

This function is an enhancer function that is provided by the Redux implementation to embed middleware and is a tool function that we often use.

export default function applyMiddleware(. middlewares) {
  return createStore= >(... args) => {conststore = createStore(... args)// Note that this dispatch is assigned using let
    // This is predefined to prevent users from using it in advance, when other middleware cannot be triggered
    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)The dispatch method cannot be used before the next function
    }
    const chain = middlewares.map(middleware= >middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch)return {
      ...store,
      dispatch
    }
  }
}
Copy the code
  • The input parameters are a list of middleware and the return value is a Store object (as opposed to the createStore code) encapsulated by the Dispatch function.
  • The compose function implements a simple onion model, with the input of the previous function as the output of the next function, as described below.
  • As you can see from the code, each middleware will input a getState and Dispatch object, and the return value will need to satisfy the compose function. For example, the time taken for each action to update state can be recorded in the following example.
function loggerMiddleware({getState, dispatch}){ // This corresponds to middleware(middlewareAPI)
    // The dispatch function cannot be used in this area, otherwise an error will be thrown!!
    return next= > action => {
        console.time(action.type);
        const result = next(action);
        // The result object is an action object. If the middleware has not changed the value, it is congruent. In general, the action should not be changed
        console.timeEnd(action.type);
        return result;  // will be passed to the next middle}}Copy the code

When writing middleware, we found that the internal closure of multiple functions, if part of the function async mode, can achieve asynchronous operation, to solve the problem of side effects, redux-thunk is a way to use this implementation, interested students can learn, only 14 lines of code, there is no discussion here.

  • The next function is the return value of the previous middleware, which is the dispatch returned by the previous middleware after encapsulation. The function of Next (Action) is equivalent to Dispatch (Action), which will trigger the subsequent middleware, so the name of Next is more vivid

compose

Compose is a function constructor that returns a new function. Similar to the functions f(x),g(x),h(x) composition of f(g(h(x))) in mathematics. The output of the previous function is the input of the next function.

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
  • Due to the limit of a single js return value, each function can have only one parameter
  • Compose (a,b,c)(x) returns a value for the compose function if x is a value. For example (subtract 50 from 500 after 10% off) :
function couponA(next) {
    if(next >= 500) {return next - 50;
    }
    return next;
}
function couponB(next){
    return next * 0.9;
}
const discount = compose(couponA, couponB);
discount(1000); / / 850
Copy the code

Boomerang cannot be implemented when the argument is a value. The above example is actually a simple chain of responsibility model, interested in in-depth mining, especially practical in e-commerce discount rules

  • Each middleware also returns a function if the argument is a function, such as Dispatch in applyMiddleware. Since Dispatch is a function, boomerang-style middleware, such as loggerMiddleware above, can take advantage of the execution characteristics of function calls to keep track of the time spent at Dispatch.
  • The compose function executes from right to left, with the last function being executed first.

combineReducers

This is a utility function that aggregates multiple reducers and returns a Reducer (which is a function)

// Reducers is oneexport default functionCombineReducers (reducers) {// omit to do a lot of checks on the reducers // below this line is for better understanding, I made up, not true source const finalReducers = {... reducers} const finalReducerKeys = Object.keys(finalReducers);functionCombination (state = {}, action) {// Some checks are omitted herelet hasChanged = false
        const nextState = {}
        for (leti = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i] const reducer = finalReducers[key] const previousStateForKey = state[key] // PreviousStateForKey is the state const nextStateForKey = Reducer (previousStateForKey, action)if (typeof nextStateForKey === 'undefined'Every reducer) {/ / there should be a return value const errorMessage = getUndefinedStateErrorMessage (key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey ! == previousStateForKey }return hasChanged ? nextState : state
    } 
}
Copy the code
  • After using combineReducers, the corresponding state also has the same structure as the Reducers object.

bindActionCreators

This function is a utility function provided by Redux. The first step is to understand the relationship between Action and Action Creator. Action is a normal object, and actionCreator is a function that constructs the action object

The purpose of bindActionCreator is to combine ActionCreator with Dispatch to create an Action method that can directly trigger a series of changes. BindActionCreators converts multiple ActionCreators into Action methods

function bindActionCreator(actionCreator, dispatch) {
  return (. args) = >dispatch(actionCreator(... args)) }export default function bindActionCreators(actionCreators, dispatch) {
  // Omit a series of checks
  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)
    }
  }
  return boundActionCreators
}
Copy the code

In practice, combining the second parameter of REat-Redux’s Connect, mapDispatchToProps, as an example, the actionCreators can be translated into a direct running method.

const actionCreators = { increase: (payload) => ({ type: 'INCREASE', payload }), decrease: (payload) => ({ type: 'DECREASE', payload }) } @connect( state => state, {... ActionCreators} /** actually executed the following code dispatch => ({actions: boundActionCreators(actionCreators, dispatch) }) **/ ) class Counter { render(){ <div> <button onClick={() => this.props.actions.increase(1)}>increase</button> <button onClick={() => this.props.actions.decrease(1)}>decrease</button> </div> } }Copy the code

conclusion

  1. Redux manages the global state through a currentState object, but splits the modified state into dipatch(Action) and Reducer, which greatly improves the flexibility and imagination of the tool library.
  2. Learn how to write redux middleware and learn more about compose function
  3. Redux is relatively complex, but it is based on the derivation of a large number of third-party tool libraries, showing its vitality, in practice experience the deep meaning of the author architecture.
  4. For ease of understanding, I’ve removed many of the type judgments that help developers debug their code better, but it’s also important not to overlook these details when exploring the source code for yourself.
  5. The article contains a large number of their own understanding, description and understanding is not appropriate, please criticize correct!!