Redux source code:

This time, I will share the source code of Redux and some of the problems I encountered while reading the source code. It is mainly divided into two parts. The first part is to realize the basic redux function, mainly including the analysis of two JS files, one is used to generate store objects, the other is used to achieve the split and merge of reduce functions. The second part is the implementation of asynchronous Dispatch, mainly through middleware functions to strengthen the implementation of Dispatch.

Implement basic Redux functionality

Let’s start with the first part, how to implement the basic Redux functionality. Below is a GIF from the Redux website that explains how redux works.

The createStore function is used to generate a store object. Store is a global state machine that holds the global state state. Add a subscription function that notifies you when the state object changes. If you want to change one of the states, issue an action with Dispatch, and Dispatch will automatically execute the Reduce function, which will recalculate on the basis of the original state and return a new state object. After the state changes, all subscription functions will be notified to change the page UI.

Createstore. js file: the basic redux file that generates the global state machine 2.com binereducer. js file that is used to split the reducer. For asynchronous function implementation

A. CreateStore. Js file

Dispatch: issue an action that changes the value of state in the store. Subscribe: Subscribe to events for state updates and notify subscription events when state changes below is the source code for createstore.js

function createStore(reducer, preloadedState, enhancer){

  // Enhance the dispatch function start
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeofenhancer ! = ='undefined') {
    // Determine that enhancer is not a function
    if (typeofenhancer ! = ='function') {
      // Throw an exception (enhancer must be a function)
      throw new Error('Expected the enhancer to be a function.')}// Call enhancer to return an enhanced store Creator
    return enhancer(createStore)(reducer, preloadedState)
  }
  // Enhance the dispatch function end
  
  let currentState = preloadedState // Current state
  let isDispatching = false // Whether the dispatch function is being executed
  let currentListeners = [] // List of current listener functions
  let nextListeners = currentListeners // A list of newly generated listener functions

  // Get the value of the variable in store
  function getState () {
    if (isDispatching) {
      throw new Error('currentState has been transferred to the Reducer function, we don't want to get the state object using getState ')}return currentState
  }

  /** Triggers the reducer function to change state to notify all subscription functions */
  function dispatch (action) {
    try {
      isDispatching = true
      // Execute reduce to change state and return a new state object
      currentState = reducer(currentState, action)
    } finally {
      isDispatching = false
    }
    // Notify all subscription functions when state changes
    let listeners = (currentListeners = nextListeners)
    for(let i = 0; i<listeners.length; i++){ listeners[i]() }return action
  }

  /** Return unsubscribe function */
  function subscribe(listener){
    let isSubscribed = true
    // ensure that changes to the nextListeners do not affect currentListeners
    ensureCanMutateNextListeners()
    // Add new listeners to the nextListeners
    nextListeners.push(listener)
    // Returns the unsubscribe function for this subscription function
    return function unsubscribe(){
      // Prevents unsubscribing twice
      if(! isSubscribed) {return 
      }
      isSubscribed = false
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index,1)
      Why / / TODO
      currentListeners = null}}// ensure that changes to the nextListeners do not affect currentListeners
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  Call Reducer to initialize the state object
  dispatch({})

  return {
    getState,
    dispatch,
    subscribe
  }
}
Copy the code

There are three main methods in the program, notes are also recorded more clearly, here will not go into detail. In the process of analyzing source code and rewriting source code, I encountered several problems, I hope to bring you a little help:

Question 1: Can the value of state in the store be changed without dispatch? Why is this not recommended? Question 2: why are currentListeners and nextListeners using store.subscribe nested listeners? Question 4: Why perform the last dispatch? Initialize state so that the state is updated with its initial value

Problem 1: I have encountered this problem in the project before and I am deeply impressed. Because the getState method returns currentState itself, not a copy of it, you can change the currentState object directly. Redux does not recommend this, however, because the subscriber function is not notified of the state change. Dispatch is the only recommended way to change state.

Problem 2: currentState has been transferred to the Reducer function, so we do not want to obtain the state object through the getState method

CurrentListeners are assigned to nextListeners at Dispatch and execute each of the currentListeners’ subscription functions. Subscribe is used to add or delete listeners and ensure that the listeners are not affected by the currentListeners. The currentListeners are only one and equal to the nextlisteners. Why can’t you just use one? I’ve found that basic functions can be achieved using nextListeners but not currentListeners. However, inconsistencies occur when subscription functions are nested within each other. Let’s take an example:

The listeners have nested a subscription function inside the currentLIsteners array, which means that a new function was added to the currentLIsteners array, resulting in inconsistent output.

As can be seen from the figure above, when the two references are the same and the two references are different, the printed results are different. The main difference is that the currentLIsteners are added to the nextListeners by the dispatch method. If the currentLIsteners and the nextListeners are the same, Add a new event to the currentLIsteners while iterating through them. So, use two subscription event sets. We don’t want to change the subscription function set at Dispatch.

Problem 4: Immediately after the store is created, an initialization action is issued to make reducer return the initialization state of the store. Otherwise, after the store is created, call getState and get undefined.

2. CombineReducers. Js method

The Reducer function is responsible for generating state. There is only one state object in the whole application, which contains all data. For a large application, state must be large, which results in the large reducer function, so the reducer function needs to be split. And in projects developed by several people, we need to write our own Reucer functions and merge them together, because the createStore can only accept a reducer function. That’s what the combineReducers function does. Here’s an example: Assume that there are two reducer functions, reducerI and reducerII functions in our project, and pass the two Reducer functions into the Combinereducers function in the form of objects. The finalReducer returned by the reducer function is passed to createStore as a Reducer function, achieving the same functions as the original Reducer function. Assume that there are two Reducer functions in our project, reducerI and reducerII, which are respectively responsible for different modules in the project. Since there is only one Reducer function passed into createStore, we need to merge the two Reducer functions into one. Pass two reducer functions into the Combinereducers function and return a synthesized Reducer function to achieve the same redux function.Running results:CombineReducers implements the function of merging the Reducer function. We are curious about how combineReducers changes its corresponding state through the passed action and merges the state into an object output. Combinereducers.js combinereducers.js

// The reducers parameter is an object, and the key value is the same as the key value of state. Value is a reducer function
function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  Filter out the values that are not the reducer function by traversing key values
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
    Filter out the value that is not the reducer function
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  // Return a synthesized reducer function (accept state and action as arguments)
  return function combination(state = {}, action) {
    
    let hasChanged = false  // Record whether the state object changes the variable
    const nextState = {}  // Next state
    // Iterate over the key value
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key] // State of the current key value
      const nextStateForKey = reducer(previousStateForKey, action) // State generated after calling reducer
      nextState[key] = nextStateForKey // Assign the newly generated state to the corresponding key of the new state
      // Indicate whether the state has changed by comparing whether previousStateForKey and nextStateForKey are equalhasChanged = hasChanged || nextStateForKey ! == previousStateForKey }// If the state changes, the new state is returned. If the state does not change, the original state object is returned
    return hasChanged ? nextState : state
  }
}
The combineReducers function is actually a reducer function that accepts state and action as arguments and returns a new state
Copy the code

CombineReducers receives reducers as input parameters and returns a synthesized Reducer function. Using closures, the reducer synthesis function is realized. In essence, actions are passed into each Reducer function in the form of traversal to obtain the initial value or change the state and return a new state object, similar to the ordinary Reducer function.

Applymiddleware.js

In a project, if the same data needs to be requested in two files, the previous practice is to request the same data in two files respectively, and then dispatch the data respectively after getting the data. Redux encapsulates the execution of the asynchronous function inside the Dispatch method. After implementing asynchronous Dispatch with Redux, it only needs to call the asynchronous method in two files. From the original dispatch that could only receive objects to the dispatch that can now execute asynchronous functions, Redux has enhanced dispatch with middleware functions to do more. How can I make the Reducer function automatically execute after the asynchronous function ends? This requires middleware to enhance the Dispatch function. Here are some examples:Running results:The above program operation and results show that after the addition of middleware Thunk, we dispatch an asynchronous operation and execute the reducer function after the completion of the asynchronous operation, achieving the desired goal. Let’s take a look at the source code for applymiddleware.js to see how middleware implements asynchronous operations. In order to understand applyMiddleware. Js, we need to know the source of the compose and reduce functions in advance:

1) Source code of reduce function

Array.prototype.myReduce = function (fn, init) {
  // The length of the array
  var len = this.length;
  var pre = init;
  var i = 0;
  // Determine whether an initial value is passed in
  if (init == undefined) {
    // No initial value is passed in. The first digit of the array defaults to the initial value. The current element index is changed to 1.
    pre = this[0];
    i = 1;
  }
  for (i; i < len; i++) {
    // The current function returns the initial value for the next time
    pre = fn(pre, this[i], i)
  }
  return pre;
}
Copy the code
var arr = [1.2.5.4.3];
var add = arr.myReduce(function (preTotal, ele, index) {
  return preTotal + ele;
}, 100)
console.log(add);/ / 115
Copy the code

It is a common example of array iterative summation realized by reduce function. When reduce is called on the array, the input parameter received by Reduce is function fn and the initial value of array iteration 100. The first execution of the function is to add the initial value 100 and the first item of array 1, and take the result of function execution 101 as the first input parameter of the function. We take the second item of the array, 2, as the second entry to the function fn, execute it a second time, and so on until we return 115 for the sum of the array and the initial value.

Executing the reduce function over an array essentially iterates through each item of the array, returning the result as an input parameter to the next execution of the function

2) source code for compose function

function compose() {
  // Copy arguments to func
  var _len = arguments.length;
  var funcs = [];
  for (var i = 0; i < _len; i++) {
    funcs[i] = arguments[i];
  }

  if (funcs.length === 0) {
    return function (arg) {
      return arg;
    };
  } 

  if (funcs.length === 1) {
    return funcs[0];
  }

  return funcs.reduce(function (a, b) {
    return function () {
      // arguments is the input argument to the return function
      return a(b.apply(undefined.arguments));
    };
  });
}
Copy the code

For example, the compose function is composed:

const value = compose(function (value) {
  return value + 1;
}, function (value) {
  return value * 2;
}, function (value) {
  return value - 3; }) (2); / / 1
Copy the code

The three functions, F1, F2 and F3, are fed into the compose function in turn. The essence of the compose function is the reduce function, which takes the value of the last function as the input parameter of the next function. The following figure shows the result of placing the parameters in the reduce function. As you can see, the array of functions coming into compose is executed from right to left. (Onion function)

3) Source code for the applyMiddleware. Js function

Now that we are familiar with the above two functions, we will tackle the last and most difficult bone, the middleware functions. Where is the middleware function used? It is passed into the createStore function as the third argument. Let’s first refine the createStore to see how it is handled when passed into the middleware function. CreateStore. Js source code:

function createStore(reducer, preloadedState, enhancer){

  // Enhance the dispatch function start
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeofenhancer ! = ='undefined') {
    // Determine that enhancer is not a function
    if (typeofenhancer ! = ='function') {
      // Throw an exception (enhancer must be a function)
      throw new Error('Expected the enhancer to be a function.')}// Call enhancer to return a newly enhanced Store creator
    return enhancer(createStore)(reducer, preloadedState)
  }
  // Enhance the dispatch function end

  // ensure that the nextListeners are consistent with currentListeners and that changes to the nextListeners do not affect currentListeners
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }
  
  let currentState = preloadedState // Current state
  let isDispatching = false // Whether dispatch is being performed
  let currentReducer = reducer // Current reducer
  let currentListeners = [] // List of current listener functions
  let nextListeners = currentListeners // A list of newly generated listener functions

  function getState () {
    // Cannot call store.getState() while executing dispatch
    if (isDispatching) {
      throw new Error('Cannot retrieve data while changing data')}return currentState
  }

  * When dispatch(aciton) is sent, the 'reducer(pure function)' used to create the store is called once. The call takes the current state and sends' action '. After the call is complete, all state listener functions are fired. * /
  function dispatch (action) {
    if (isDispatching) {
      throw new Error('You can't change the data at the same time')}try {
      isDispatching = true
      // Execute reduce to change state and return a new state object
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
    // Notify all subscription functions when state changes
    let listeners = (currentListeners = nextListeners)
    for(let i = 0; i<listeners.length; i++){ listeners[i]() }return action
  }

  // Add a change listener and return a unbinding function that executes inside dispatch. All listeners will be fired once after dispatch(action)
  function subscribe(listener){
    if (isDispatching) {
      throw new Error('Cannot subscribe to events while changing data')}let isSubscribed = true
    ensureCanMutateNextListeners()
    // Add new listeners to the nextListeners
    nextListeners.push(listener)
    // Returns the unsubscribe function for this subscription function
    return function unsubscribe(){
      if(! isSubscribed) {return 
      }
      isSubscribed = false
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index,1)}}// Initialize the state object
  // dispatch({})

  return {
    getState,
    dispatch,
    subscribe
  }
}
Copy the code

Here is the main line of code after adding the enhancer:

return enhancer(createStore)(reducer, preloadedState)
Copy the code

Now let’s paste applymiddleware.js code and see what middleware does.

// enhancer(createStore)(reducer, preloadedState) = applyMiddleware(... middlewares)
function applyMiddleware(. middlewares) {
  ApplyMiddleware returns an enhancer function
  return  (createStore) = > {
    The enhancer function returns a function that generates a store
    return (. args) = > {
      // Generate the basic store function
      conststore = createStore(... args)let dispatch = () = > {
        throw new Error(
          `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`)}// An input parameter required by middleware
      const middlewareAPI = {
        getState: store.getState,
        dispatch: (. args) = >dispatch(... args) }// Inject the middlewareAPI into the middleware to get an array of second-layer middleware functions
      const chain = middlewares.map(middleware= >middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch)return {
        ...store,
        dispatch
      }
    }
  } 
}
Copy the code

Compare the previous asynchronous execution:The applyMiddleware function returns an enhancer function that takes a createStore function as an input parameter. After the enhancer function is executed, the return function receives reducer and preloadedState as input parameters. After this function is executed, the return is a store object with the enhanced disptach method. Let’s look at the bottom half of the applyMiddleware function:In the figure above, the main thing is to generate the basic Store object, enhance the Dispatch function with middleware, and return a new store object. So how does the middleware function enhance the Dispatch function? Let’s use two middleware functions as examples:Thunk and Logger are two of the most common middleware functions in the React-Redux package. They handle asynchronous operations and print states, respectively. They have a common format: ({dispatch,getState}) =>{return next => Action =>{}} applyMiddleware gets the chain array, which is the second layer of functions that inject Dispatch and getState into middleware functions. Next => action => {} The following f1 f2 function is a function in the chain array after passing the middleware input parameter to the middleware function

Next, to the most important step:

After executing the code, insert f1 and f2 into the code and get the new dispatch:You can see that dispatch has really been enhanced, not only for asynchrony, but also for printing state before and after Dispatch.We use the new dispatch to execute the asynchronous function. The program determines that the action is a function, enters the upper part and starts to execute the asynchronous function. After setTimeout is executed, it dispatches again and enters the lower part again to print the state before dispatch. Then call Store. dispatch (the original dispatch) to update the state, and finally print the state after dispatch. The final execution result is:You can see that dispatch has really been enhanced, not only for asynchrony, but also for printing state before and after Dispatch.

So far, we have parsed the source code of Redux, hoping to give you a little help.