In the last article, we looked at the use of Redux and the role of the various functional modules through an example page. If you’re not sure how Redux works, you can make it easier to understand by looking at how simple Redux is (the example article) and then reading this article.

In this article, I will take you to write a complete Redux, in-depth analysis of all aspects of Redux, after reading this article, you will have a deep understanding of Redux.

Core API

This set of code is the author read Redux source code, after understanding its design ideas, summed up a set of code, API design follows the same principle as the original, omitted some unnecessary API.

createStore

This method is the heart of the Redux core, linking all the other functions together and exposing the API of the operation for developers to call.

const INIT = '@@redux/INIT_' + Math.random().toString(36).substring(7)

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

  let state = initialState
  const listeners = []
  const store = {
    getState () {
      return state
    },
    dispatch (action) {
      if (action && action.type) {
        state = reducer(state, action)
        listeners.forEach(listener= > listener())
      }
    },
    subscribe (listener) {
      if (typeof listener === 'function') {
        listeners.push(listener)
      }
    }
  }

  if (typeof initialState === 'undefined') {
    store.dispatch({ type: INIT })
  }

  if (typeof enhancer === 'function') {
    return enhancer(store)
  }

  return store
}
Copy the code

At initialization, the createStore will actively trigger dispach. Its action. Type is a built-in INIT, so the Reducer does not match any action. The goal is to get the initialization state.

Of course, we can also manually specify the initialState, the author made a judgment here, when the initialState is not defined, we will dispatch, but in the source code is always a dispatch, the author thinks it is unnecessary, this is an unnecessary operation. Because at this time, no function was registered in the listening flow, we went through the default logic in reducer, and the new state and initialState were the same.

The third parameter, enhancer, is used only when middleware is used, usually with applyMiddleware. It enhances Dispatch, such as Logger and Thunk, which enhance Dispatch.

CreateStore also returns some action apis, including:

  • GetState: Gets the current state value
  • Dispatch: The Reducer is triggered and each method in the Process is executed
  • Subscribe: Registers methods to listeners, triggered by dispatches

applyMiddleware

This approach enhances dispatch through middleware.

Before writing any code, let’s take a look at the composition of functions to help you understand how applyMiddleware works.

Composition of functions

If a value passes through multiple functions to become another value, you can combine all intermediate steps into a single function, which is called composing the function.

For example

function add (a) {
  return function (b) {
    return a + b
  }
}

// Get the synthesized method
let add6 = compose(add(1), add(2), add(3))

add6(10) / / 16
Copy the code

Now let’s write a function composition in a very clever way.

export 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

The clever part of the code above is that we combine the two methods into one method by using the reduce method of the array, and then combine that method with the next method until we end up with a composite function of all methods.

With this base, applyMiddleware becomes very simple.

import { compose } from './utils'

export default function applyMiddleware (. middlewares) {
  return store= > {
    const chains = middlewares.map(middleware= >middleware(store)) store.dispatch = compose(... chains)(store.dispatch)return store
  }
}
Copy the code

This code may be a bit confusing to look at alone, but we work with the middleware code structure to help us understand

function middleware (store) {
  return function f1 (dispatch) {
    return function f2 (action) {
      // do something
      dispatch(action)
      // do something}}}Copy the code

Chains are an array of functions F1. Compose merges the desired F1 into a function, let’s call it F1 for now. Then we pass the original dispatch into F1. This process is the same as Koa’s middleware model (the Onion Model).

To help you understand, let’s take another example. We have the following two middleware

function middleware1 (store) {
  return function f1 (dispatch) {
    return function f2 (action) {
      console.log(1)
      dispatch(action)
      console.log(1)}}}function middleware2 (store) {
  return function f1 (dispatch) {
    return function f2 (action) {
      console.log(2)
      dispatch(action)
      console.log(2)}}}// applyMiddleware(middleware1, middleware2)
Copy the code

Can you guess what the order of the log output is?

All right, answer: 1, 2, (original dispatch), 2, 1.

Why is that? Because the dispatch received by Middleware2 is the most original, and the dispatch received by Middleware1 is transformed by Middleware1, I write them as follows, so that you can understand clearly.

console.log(1)

/* Middleware1 returns to middleware2's dispatch */
console.log(2)
dispatch(action)
console.log(2)
/* end */

console.log(1)
Copy the code

The same is true for three or more pieces of middleware.

So far, the most complex and difficult to understand middleware has been explained.

combineReducers

Since Redux is a single state flow management mode, if there are multiple reducer, we need to merge the reducer. The logic of this reducer is relatively simple, so we directly load the code.

export default function combineReducers (reducers) {
  const availableKeys = []
  const availableReducers = {}

  Object.keys(reducers).forEach(key= > {
    if (typeof reducers[key] === 'function') {
      availableKeys.push(key)
      availableReducers[key] = reducers[key]
    }
  })

  return (state = {}, action) = > {
    const nextState = {}
    let hasChanged = false

    availableKeys.forEach(key= > {
      nextState[key] = availableReducers[key](state[key], action)

      if(! hasChanged) { hasChanged = state[key] ! == nextState[key] } })return hasChanged ? nextState : state
  }
}
Copy the code

CombineReucers puts a single Reducer into an object. Each Reducer has a unique key value. When the single Reducer state changes, the value of the corresponding key value also changes and the whole state is returned.

bindActionCreators

This method is to connect our action to our Dispatch.

function bindActionCreator (actionCreator, dispatch) {
  return function () {
    dispatch(actionCreator.apply(this.arguments))}}export default function bindActionCreators (actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  const boundActionCreators = {}

  Object.keys(actionCreators).forEach(key= > {
    let actionCreator = actionCreators[key]

    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  })

  return boundActionCreators
}
Copy the code

It returns a collection of methods that are called directly to trigger dispatch.

The middleware

When you write your own middleware, you will be surprised to find that such a good middleware code is only a few lines, but can achieve such a powerful function.

logger

function getFormatTime () {
  const date = new Date(a)return date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds() + ' ' + date.getMilliseconds()
}

export default function logger ({ getState }) {
  return next= > action => {
    /* eslint-disable no-console */
    console.group(`%caction %c${action.type} %c${getFormatTime()}`.'color: gray; font-weight: lighter; '.'inherit'.'color: gray; font-weight: lighter; ')
    // console.time('time')
    console.log(`%cprev state`.'color: #9E9E9E; font-weight: bold; ', getState())
    console.log(`%caction `.'color: #03A9F4; font-weight: bold; ', action)

    next(action)

    console.log(`%cnext state`.'color: #4CAF50; font-weight: bold; ', getState())
    // console.timeEnd('time')
    console.groupEnd()
  }
}
Copy the code

thunk

export default function thunk ({ getState }) {
  return next= > action => {
    if (typeof action === 'function') {
      action(next, getState)
    } else {
      next(action)
    }
  }
}
Copy the code

One thing to note here is that middleware has an execution order. In this case, the first argument is thunk, followed by logger, because if logger comes first, then the action may be an asynchronous method and will not output the action properly.

tips

At this point, all aspects of Redux have been covered, and I hope you found them useful.

But I actually have one concern: Every dispatch re-renders the entire view. Although React diff on the virtual DOM and then render the real DOM that needs to be updated, we know that Redux scenarios are generally medium and large applications that manage huge amounts of state data. At this time, the whole virtual DOM diff process may cause significant performance loss (diff process is actually the object and each field of the object comparison, if the data reaches a certain level, even without operating the real DOM, may also cause considerable performance loss. In small applications, due to the small amount of data, Therefore, the performance loss of diff is negligible).

This article source address: github.com/ansenhuang/…