Implement a Redux (perfect version)

Time:2020-9-26

Last time, we have written to implement a mini Redux (basic version). This time, we will continue to improve Redux and continue to write the example in the previous article.

middleware

Redux has an API called applymiddleware, which is specially used to use middleware. First of all, we have to know what it is used for.

Why middleware is needed?

Suppose we need to record the state records before and after each dispatch, what should we do? So, simply add code before and after the first dispatch method

console.log('prev state', store.getState())
console.log(action)
store.dispatch({ type: 'INCREMENT' })
console.log('next state', store.getState())

This part of the running results are as follows:

prev state {value: 10}
{type: "INCREMENT"}
The current number is: 11
next state {value: 11}

However, after adding, it is found that the situation is wrong. If the page has multiple dispatches, it will generate a lot of duplicate code if you want to write it for many times. All of a sudden, there is a need to record the reasons for each error. The separate function requirements are as follows:

try{
    store.dispatch(action)   
}catch(err){
    console.error ('error message: ', ERR)  
}

Then we need both of them, so we can make two of them, but it’s even more chaotic when we stack them together.

The concept of Middleware

Obviously, we can’t do it in this way. The ideal solution is that Redux itself provides a function entry, so that we can add functions outside, so that the code will not be complicated.

But if we add functions to our existing Redux implementation, which link is more appropriate?

  • Reducer: pure function, which only undertakes the function of calculating state. It is not suitable to undertake other functions, nor can it undertake it. In theory, pure functions cannot perform read and write operations.
  • View: corresponding to state one by one, it can be regarded as the visual layer of state, and it is not suitable for other functions.
  • Action: the object that stores the data, that is, the carrier of the message, can only be operated by others and cannot be operated by itself.

We find that the above requirements are all related to dispatch, and only the step of sending action, that is store.dispatch () method, you can add functions. For example, to add the log function, we only need to put the log into the dispatch function. We only need to modify the dispatch function to encapsulate the dispatch function.

const store = createStore(counter)
const next = store.dispatch
store.dispatch = (action) => {
    try{
        console.log('prev state', store.getState())
        console.log(action)
        next(action)   
        console.log('next state', store.getState())
    }catch(err){
        console.error ('error message: ', ERR)  
    }
}

Code above, right store.dispatch This is the prototype of middleware.

Therefore, Redux middleware is a function, which is an extension of the dispatch method and enhances the function of dispatch.

Implement Middleware

In fact, the package of the above dispatch is very defective. What if there are more than N requirements? The dispatch function will be too confusing to maintain, so we need a multi middleware cooperation mode with strong scalability.

  1. We extract loggermiddleware
const store = createStore(counter)
const next = store.dispatch

const loggerMiddleware = (action) => {
  console.log('prev state', store.getState())
  console.log(action)
  next(action)
  console.log('next state', store.getState())
}

store.dispatch = (action) => {
  try {
    loggerMiddleware(action)
  } catch (err) {
    console.error ('error message: ', ERR)
  }
}
  1. Extract exceptionmiddleware
const exceptionMiddleware = (action) => {
  try {
    loggerMiddleware(action)
  } catch (err) {
    console.error ('error message: ', ERR)
  }
}

store.dispatch = exceptionMiddleware
  1. Now there is a problem with the code, that is, the exception middleware writes loggermiddleware to death, but in case of not recording the function in the future, we need to make the next (action) dynamic, that is, any middleware can be changed
const exceptionMiddleware = (next) => (action) => {
  try {
    // loggerMiddleware(action)
    next(action)
  } catch (err) {
    console.error ('error message: ', ERR)
  }
}

This method may not be suitable at the beginning. In fact, it returns a function in a function, which is equivalent to

const exceptionMiddleware = function (next) {
  return function (action) {
    try {
      // loggerMiddleware(action)
      next(action)
    } catch (err) {
      console.error ('error message: ', ERR)
    }
  }
}

Exception middleware (next) (action) is passed

  1. Similarly, we can’t extend other middleware in loggermiddleware! We also write next as dynamic
const loggerMiddleware = (next) => (action) => {
  console.log('prev state', store.getState())
  console.log(action)
  next(action)
  console.log('next state', store.getState())
}

So far, the whole middleware design and transformation are as follows:

const store = createStore(counter)
const next = store.dispatch

const loggerMiddleware = (next) => (action) => {
  console.log('prev state', store.getState())
  console.log(action)
  next(action)
  console.log('next state', store.getState())
}

const exceptionMiddleware = (next) => (action) => {
  try {
    next(action)
  } catch (err) {
    console.error ('error message: ', ERR)
  }
}

store.dispatch = exceptionMiddleware(loggerMiddleware(next))
  1. Now there is a new problem. If you think about the introduction of middleware from the outside, how can it be found in the external middleware store.getState () this method, so we put the store independently.
const store = createStore(counter)
const next = store.dispatch

const loggerMiddleware = (store) => (next) => (action) => {
  console.log('prev state', store.getState())
  console.log(action)
  next(action)
  console.log('next state', store.getState())
}

const exceptionMiddleware = (store) => (next) => (action) => {
  try {
    next(action)
  } catch (err) {
    console.error ('error message: ', ERR)
  }
}

const logger = loggerMiddleware(store)
const exception = exceptionMiddleware(store)
store.dispatch = exception(logger(next))
  1. If there is a new requirement, we need to output the current timestamp before printing the log, and we need to construct a middleware
const timeMiddleware = (store) => (next) => (action) => {
  console.log('time', new Date().getTime())
  next(action)
}

const logger = loggerMiddleware(store)
const exception = exceptionMiddleware(store)
const time = timeMiddleware(store)
store.dispatch = exception(time(logger(next)))

Optimization of middleware usage

It can be seen from the above writing method that the use of middleware is a little cumbersome, so we need to encapsulate the details and implement it by extending the createstore.
Let’s first look at the expected usage:

/*Receive the old createstore and return the new one*/
const newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore);

/*A store whose dispatch has been rewritten is returned*/
const store = newCreateStore(reducer);

Implementation of applymiddleware

export const applyMiddleware = function (...middlewares) {
  /*Returns a method that overrides the createstore*/
  return function rewriteCreateStoreFunc(oldCreateStore) {
    /*Returns the new createstore after rewriting*/
    return function newCreateStore(reducer, preloadedState) {
      //Generate store
      const store = oldCreateStore(reducer, preloadedState)
      let dispatch = store.dispatch

      //Only the API of the store part for middleware is exposed, and the whole store is not passed in
      const middlewareAPI = {
        getState: store.getState,
        dispatch: (action) => store.dispatch(action),
      }
      //Pass in the API to each middleware
      //Equivalent to const logger = loggermiddleware (store), namely const logger = loggermiddleware ({getstate, dispatch})
      // const chain = [exception, time, logger]
      const chain = middlewares.map((middleware) => middleware(middlewareAPI))
      //Implement exception (time ((logger (dispatch))))
      chain.reverse().map((middleware) => {
        dispatch = middleware(dispatch)
      })
      //Override dispatch
      store.dispatch = dispatch
      return store
    }
  }
}

Let’s look at this Code:

chain.reverse().map((middleware) => {
    dispatch = middleware(dispatch)
})

It should be noted that middleware is executed sequentially, but dispatch is generated in reverse order. For example, when calling distware a, we will know the order of dispatch B (for example, when we call dispatch B, we need to know the order of dispatch B, so we need to know the order of C when we need to execute a)

The official Redux source code uses the compose function. We also try this way to write:

export const applyMiddleware = (...middlewares) => {
  return (createStore) => (...args) => {
    // ...
    dispatch = compose(...chain)(store.dispatch)
    // ... 
  }
}

// compose(fn1, fn2, fn3)
// fn1(fn2(fn3))
//Combining multiple functions from right to left: combining received functions from right to left is the final function
export const compose = (...funcs) => {
  if (funcs.length === 0) {
    return (arg) => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((ret, item) => (...args) => ret(item(...args)))
}

Let’s simplify the code

export const applyMiddleware = (...middlewares) => {
  return (createStore) => (...args) => {
    const store = createStore(...args)
    let dispatch = store.dispatch

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action),
    }

    const chain = middlewares.map((middleware) => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch,
    }
  }
}

export const compose = (...funcs) => {
  if (funcs.length === 0) {
    return (arg) => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((ret, item) => (...args) => ret(item(...args)))
}

The processing of createstore

The problem now is that there are two createstores. How to distinguish them? In the first part, we have already told us how to handle the middleware code, but we will continue to see how to launch it.

//Create store without Middleware
const store = createStore(counter)

//Create store with middleware
const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware);
const newCreateStore = rewriteCreateStoreFunc(createStore);
const store = newCreateStore(counter, preloadedState);

In order to make users use it in a unified way, we can simply make their usage consistent. We modify the createstore method

const createStore = (reducer, preloadedState, rewriteCreateStoreFunc) => {
    //If you have rewritecreatestorefunc, use the new createstore 
    if(rewriteCreateStoreFunc){
       const newCreateStore =  rewriteCreateStoreFunc(createStore);
       return newCreateStore(reducer, preloadedState);
    }
    // ...
}

However, the Redux source code rewritecreatestorefunc has changed its name and added a judgment, which is the code in our previous article:

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

So the usage of middleware is

Const store = createstore (counter, / * preloadedstate optional * / applymeddleware (logger))

combineReducers

If we do a large project with a large number of States, then maintenance is very troublesome. Redux provides the combinereducers method, which is used to merge multiple reducers into one reducer, and each reducer is responsible for its own module.

Let’s use a new example

import { createStore, applyMiddleware, combineReducers } from 'redux'

const initCounterState = {
  value: 10,
}
const initInfoState = {
  name: 'jacky',
}

const reducer = combineReducers({
  counter: counterReducer,
  info: infoReducer,
})

//Counter reducer processing function
function counterReducer(state = initCounterState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        value: state.value + 1,
      }
    case 'DECREMENT':
      return {
        ...state,
        value: state.value - 1,
      }
    default:
      return state
  }
}

function infoReducer(state = initInfoState, action) {
  switch (action.type) {
    case 'FULL_NAME':
      return {
        ...state,
        name: state.name + ' lin',
      }
    default:
      return state
  }
}

const store = createStore(reducer)

const init = store.getState()
//At first, counter was 10, info was Jacky
console.log (at first, the counter was:${ init.counter.value }, info is${ init.info.name >)
function listener() {
  store.getState()
}
store.subscribe (listener) // listen for changes in state

// counterReducer
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'DECREMENT' })

// infoReducer
store.dispatch({ type: 'FULL_NAME' })

//After execution, counter is 11 and info is Jacky Lin
console.log (after execution, the counter is:${ store.getState (). counter.value }, info is${ store.getState (). info.name >)
export default store

Let’s try how to implement this API,

First, all the reducers loops in a function should be executed once, and the function should follow the (state, action) = > newstate format. We also need to merge the initstate of each reducer into a rootstate.
The implementation is as follows:

export function combineReducers(reducers) {
  // reducerKeys = ['counter', 'info']
  const reducerKeys = Object.keys(reducers)
  //Returns the merged new reducer function
  return function combination(state = {}, action) {
    //New state generated
    const nextState = {}

    //Traverse all reducers and integrate them into a new state
    for (let i = 0; i < reducerKeys.length; i++) {
      const key = reducerKeys[i]
      const reducer = reducers[key]
      //State of previous key
      const previousStateForKey = state[key]
      //Execute the sub reducer to get the new state
      const nextStateForKey = reducer(previousStateForKey, action)

      nextState[key] = nextStateForKey
    }
    return nextState
  }
}

replaceReducer

In large web applications, it is usually necessary to split the application code into multiple JS packages that can be loaded on demand. This strategy, called code partitioning, improves application performance by reducing the size of the JS package when it is first loaded.

After splitting the reducer, it corresponds to the components one by one. We hope that when doing on-demand loading, the reducer can also follow the components to load again when necessary, and then replace the old reducer with a new one. But in fact, there is only one root reducer function. If you want to implement it, you can use the replacereducer function. The implementation is as follows:

const createStore = function (reducer, initState) {
  // ...
  const replaceReducer = (nextReducer) => {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    reducer = nextReducer
    //Refresh the value of state, and the new reducer will put its default state in the state tree
    dispatch({ type: Symbol() })
  }
  // ...
  return {
    // ...
    replaceReducer
  }
}

Use as follows:

const reducer = combineReducers({
  counter: counterReducer
});
const store = createStore(reducer);

/*Generate a new reducer*/
const nextReducer = combineReducers({
  counter: counterReducer,
  info: infoReducer
});
/*replaceReducer*/
store.replaceReducer(nextReducer);

bindActionCreators

Bindactioncreators are rarely used, but are used in the connect function implementation of react redux

The scenario where bindactioncreators will be used is when you need to pass the action creator down to a component, but you don’t want this component to be aware of the existence of Redux, and you don’t want to pass the dispatch or Redux store to it.

Let’s try hiding dispatch and actioncreator in a common way

const reducer = combineReducers({
  counter: counterReducer,
  info: infoReducer
});
const store = createStore(reducer);

//The function that returns action is called actioncreator
function increment() {
  return {
    type: 'INCREMENT'
  }
}

function getName() {
  return {
    type: 'FULL_NAME',
  }
}

const actions = {
  increment: function () {
    return store.dispatch(increment.apply(this, arguments))
  },
  getName: function () {
    return store.dispatch(getName.apply(this, arguments))
  }
}
//Other places do not know the details of dispatch and actioncreator when implementing auto increment
actions.increment (); // Auto increment
actions.getName (); // get full name

Extract the public code when actions are generated

const actions = bindActionCreators({ increment, getName }, store.dispatch)

The implementation of bindactioncreators is as follows:

//Return the function of wrapping dispatch and convert actioncreator to the form of dispatch
// eg. { addNumAction }  =>  (...args) => dispatch(addNumAction(args))
export function bindActionCreator(actionCreator, dispatch) {
  return function (...args) {
    return dispatch(actionCreator.apply(this, args))
  }
}

export function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }
  //Actioncreators must be function or object
  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error()
  }

  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
}

Maybe you are a little confused here. Let’s recall the use of the connect function in react redux,
For example, there is an actioncreator

// actionCreators.js
function addNumAction() {
    return { type: 'ADD_NUM' }
}
// Demo.js : at the bottom of components that need store data, such as demo, we use the connect function to connect, as follows:
import { addNumAction } from './actionCreators'
const mapDispatchToProps = (dispatch) => ({
  addNum() {
    dispatch(addNumAction())
  }
})
export default connect(mapStateToProps, mapDispatchToProps)(Demo)

Then use the button on the page to start, and the action is add_ Num corresponding event

<button onClick={ this.props.addNum }>Add 1 < / button > to

But in addition to the above usage, mapdispatchtoprops can also be used in this way. An object is directly passed in without the dispatch method

export default connect(mapStateToProps, { addNumAction })(Demo)

Then just trigger addnumaction to achieve the same effect as above.

Why not? When you pass in an object, the connect function will judge. The code is as follows:

let dispatchToProps

if (typeof mapDispatchToProps === 'function') {
    dispatchToProps = mapDispatchToProps(store.dispatch)
} else {
    //An actioncreator object was passed in
    dispatchToProps = bindActionCreators(mapDispatchToProps, store.dispatch)
}

Here we use the bindactioncreators function, which is to package the action creator you passed in with a layer of dispatch method, that is

{ addNumAction }  =>  (...args) => dispatch(addNumAction(args))

summary

This is the end of the Redux implementation. After understanding the principle, the understanding of Redux is really deepened. After that, we will continue to write the implementation of related plug-ins, such as react redux.

reference material:

Fully understand Redux (implementing a Redux from zero)

  • PS: personal technology blog GitHub warehouse, if you think it’s good, welcome star, give me some encouragement~

Recommended Today

Explain idea git branch backoff specified historical version

scene When I submitted this modification to the local and remote branches, I found that there were still some changes missing in this submission, or this modification was totally wrong, but I also pushed it to the remote repository. How to go back? problem How can the content that has been submitted to the repository […]