Redux is a well-known library that is used in many places, and I have been using it for several years. This article is to implement Redux myself, so as to understand its principle in depth. We’ll start with the basics and implement a Redux to replace the source NPM package, but with the same functionality. This article will only implement the core library of Redux, and use it with other libraries, such as React-Redux, in a separate article. Sometimes we focus too much on usage and only remember the various ways of use, instead of their core principles. However, if we want to really improve the technology, it is better to figure out one by one. For example, Redux and React-Redux look similar, but their core concepts and concerns are different. Redux is a simple state management library with no interface. Redux focuses on how to combine Redux with React, using some React apis.

The entire code has been uploaded to GitHub for you to play with:Github.com/dennis-jian…

The basic concept

The concept of Redux has been written about a lot, and I’m not going to expand it, but I’m just going to mention it. The basic concepts of Redux are as follows:

Store

As its name suggests, a Store is a repository that stores all the states and provides some APIS for manipulating them. Our subsequent operations are actually operating on this repository. If our warehouse is used to Store milk, initially there is not a case of milk in our warehouse, then the State of Store is:

{
	milk: 0
}
Copy the code

Actions

An Action is an Action that changes the state of a Store. The Store is the same Store. Now I want to put a box of milk in the Store.

{
  type: "PUT_MILK".count: 1
}
Copy the code

Reducers

Reducer is to change the state in the Store according to the received actions. For example, I received a PUT_MILK and the count was 1, then the milk added was increased by 1. From 0 to 1, the code looks like this:

const initState = {
  milk: 0
}

function reducer(state = initState, action) {
  switch (action.type) {
    case 'PUT_MILK':
      return{... state,milk: state.milk + action.count}
    default:
      return state
  }
}
Copy the code

It can be seen that Redux itself is a simple state machine. The Store stores all states and the Action is a state change notification. Reducer changes the corresponding state in the Store upon receiving the notification.

A simple example

Let’s look at a simple example that includes the aforementioned concepts of Store, Action and Reducer:

import { createStore } from 'redux';

const initState = {
  milk: 0
};

function reducer(state = initState, action) {
  switch (action.type) {
    case 'PUT_MILK':
      return{... state,milk: state.milk + action.count};
    case 'TAKE_MILK':
      return{... state,milk: state.milk - action.count};
    default:
      returnstate; }}let store = createStore(reducer);

// subscribe is a change in the store. Once the store changes, the callback is called
// If it is combined with page update, the update operation is performed here
store.subscribe(() = > console.log(store.getState()));

// Dispatch the action with dispatch
store.dispatch({ type: 'PUT_MILK' });    // milk: 1
store.dispatch({ type: 'PUT_MILK' });    // milk: 2
store.dispatch({ type: 'TAKE_MILK' });   // milk: 1
Copy the code

Their implementation

The previous example was short but covered the core functionality of Redux, so our first goal in writing was to replace Redux in this example. To replace Redux, we need to know what’s in it first, and on closer inspection, it looks like we’re using only one of its apis:

CreateStore: This API takes the Reducer method as a parameter and returns a store on which the main functionality resides.

Take a look at what we’ve used on store:

Store. Subscribe: subscribe state changes, when the state changes to execute the callback, can have multiple subscribe, the callback will be executed in sequence.

Store. Dispatch: Issue an action. Each dispatch action executes a reducer to generate a new state, and then executes a subscribe register callback.

Store.getstate: a simple method that returns the current state.

Subscribe registers a callback, Dispatch triggers a callback, and what do you think, isn’t that publish-subscribe? I talked about the publish-subscribe model in detail in a previous article, so here’s a direct copy.

function createStore() {
  let state;              // state Records all states
  let listeners = [];     // Save all registered callbacks

  function subscribe(callback) {
    listeners.push(callback);       // subscribe saves the callback
  }

  // Dispatch is to take all callbacks out and execute them one by one
  function dispatch() {
    for (let i = 0; i < listeners.length; i++) {
      constlistener = listeners[i]; listener(); }}// getState returns state directly
  function getState() {
    return state;
  }

  // store wraps the previous method and returns directly
  const store = {
    subscribe,
    dispatch,
    getState
  }

  return store;
}
Copy the code

Redux is a publish-subscribe model at its core. It’s as simple as that. Wait, I think we missed something. What about Reducer? The function of reducer is to change state when events are released. Therefore, before callback, our dispatch should first perform reducer and re-assign state with the return value of reducer. Rewrite the dispatch as follows:

function dispatch(action) {
  state = reducer(state, action);

  for (let i = 0; i < listeners.length; i++) {
    constlistener = listeners[i]; listener(); }}Copy the code

At this point, we have implemented all the apis used in the previous example. Let’s try replacing the official Redux with our own:

// import { createStore } from 'redux';
import { createStore } from './myRedux';
Copy the code

We can see that the output is the same, indicating that there is no problem with our own Redux:

Knowing Redux’s core principles, we should have no problem looking at his source code, createStore’s source code portal.

Finally, let’s take a look at the core flow of Redux. Note that pure Redux is just a state machine, and there is no View layer.

In addition to this core logic, there are some interesting apis in Redux that we’ll write by hand.

handwrittencombineReducers

CombineReducers is also a very wide range of apis. As the application becomes more and more complex, if all the logic is written in a reducer, the final file may have thousands of rows, so Redux provides combineReducers. Let us write our own Reducer for the different modules and eventually combine them. For example, due to the good business development of the milk warehouse at the beginning, we have added a rice warehouse. We can create our reducer for these two warehouses and then combine them, using the following methods:

import { createStore, combineReducers } from 'redux';

const initMilkState = {
  milk: 0
};
function milkReducer(state = initMilkState, action) {
  switch (action.type) {
    case 'PUT_MILK':
      return{... state,milk: state.milk + action.count};
    case 'TAKE_MILK':
      return{... state,milk: state.milk - action.count};
    default:
      returnstate; }}const initRiceState = {
  rice: 0
};
function riceReducer(state = initRiceState, action) {
  switch (action.type) {
    case 'PUT_RICE':
      return{... state,rice: state.rice + action.count};
    case 'TAKE_RICE':
      return{... state,rice: state.rice - action.count};
    default:
      returnstate; }}// Combine two reducer using combineReducers
const reducer = combineReducers({milkState: milkReducer, riceState: riceReducer});

let store = createStore(reducer);

store.subscribe(() = > console.log(store.getState()));

// Take action at 🥛
store.dispatch({ type: 'PUT_MILK'.count: 1 });    // milk: 1
store.dispatch({ type: 'PUT_MILK'.count: 1 });    // milk: 2
store.dispatch({ type: 'TAKE_MILK'.count: 1 });   // milk: 1

// Action for the rice
store.dispatch({ type: 'PUT_RICE'.count: 1 });    // rice: 1
store.dispatch({ type: 'PUT_RICE'.count: 1 });    // rice: 2
store.dispatch({ type: 'TAKE_RICE'.count: 1 });   // rice: 1
Copy the code

In the above code, we divided the large state into two small milkState and riceState, and the final running result is as follows:

Knowing how to use it, we tried to write it down ourselves! To write combineReducers, let’s analyze what it did. First, its return value is a reducer, which is also passed as a reducer parameter of createStore, indicating that this return value is a function like the normal reducer structure we had before. This function also takes state and action and returns the new state, but the new state conforms to the data structure of the combineReducers parameter. Let’s try to write:

function combineReducers(reducerMap) {
  const reducerKeys = Object.keys(reducerMap);    // Take all the keys out of the argument
  
  // The return value is a reducer function of a normal structure
  const reducer = (state = {}, action) = > {
    const newState = {};
    
    for(let i = 0; i < reducerKeys.length; i++) {
      // The value of each key in reducerMap is a reducer. We can get the new state value of corresponding key by taking it out and running it
      // Assemble all states returned by the reducer according to the keys in the parameters
      // Finally return the assembled newState
      const key = reducerKeys[i];
      const currentReducer = reducerMap[key];
      const prevState = state[key];
      newState[key] = currentReducer(prevState, action);
    }
    
    return newState;
  };
  
  return reducer;
}
Copy the code

The implementation principle of the official source code is the same as ours, but he has more error handling, you can look at the comparison.

handwrittenapplyMiddleware

Middleware is a very important concept in Redux. The middleware ecosystem of Redux depends on this API. For example, if you want to write a logger middleware, you can say:

Logger is middleware. Note that the return value has several layers of functions embedded
// Why do we do this
function logger(store) {
  return function(next) {
    return function(action) {
      console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result
    }
  }
}

// Pass applyMiddleware as the second parameter to createStore
const store = createStore(
  reducer,
  applyMiddleware(logger)
)
Copy the code

As you can see, in order to support middleware, createStore supports a second parameter, officially called enhancer, which as its name implies is an enhancer used to enhance the store’s capabilities. The official definition of enhancer is as follows:

type StoreEnhancer = (next: StoreCreator) = > StoreCreator
Copy the code

If enhancer is a function, it must receive a StoreCreator function and return a StoreCreator function. Note that its return value is also a StoreCreator, that is, we should get the same return structure as the createStore, that is, whatever structure our createStore returns, it must also return, that is, the store:

{
  subscribe,
  dispatch,
  getState
}
Copy the code

createStoresupportenhancer

Alter createStore to support enhancer:

function createStore(reducer, enhancer) {   // Receive the second parameter, enhancer
  // Start with enhancer
  // If enhancer exists and is a function
  // We pass createStore as a parameter
  // He should return a new createStore to me
  // I take the new createStore execution and should get a store
  // Just return the store
  if(enhancer && typeof enhancer === 'function') {const newCreateStore = enhancer(createStore);
    const newStore = newCreateStore(reducer);
    return newStore;
  }
  
  // If there is no enhancer or enhancer is not a function, execute the previous logic directly
  // The following code is the same as the previous version
  // omit n lines of code
	/ /...
  const store = {
    subscribe,
    dispatch,
    getState
  }

  return store;
}
Copy the code

See the corresponding source code for this section here.

applyMiddlewareThe return value is oneenhancer

Now that we have the basic structure of enhancer, applyMiddleware is passed to createStore as a second parameter, meaning that it is an enhancer. Because we pass it to createStore as a result of its execution applyMiddleware() :

function applyMiddleware(middleware) {
  // applyMiddleware should return an enhancer
  // The enhancer parameter is createStore as we said earlier
  function enhancer(createStore) {
    // enhancer should return a new createStore
    function newCreateStore(reducer) {
      // We write an empty newCreateStore to return the createStore result
      const store = createStore(reducer);
      return store
    }
    
    return newCreateStore;
  }
  
  return enhancer;
}
Copy the code

implementationapplyMiddleware

The basic structure of applyMiddleware is already there, but its functionality is not yet implemented. To implement this middleware, we need to know what it can do.

function logger(store) {
  return function(next) {
    return function(action) {
      console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result
    }
  }
}
Copy the code

This middleware runs as follows:

Let result = next(action); After this line is executed, the state changes. We said that the only way to change the state is dispatch(action), so next(action) is dispatch(action), just with a different name. Return function(action) returns function(action), action (action) returns function(action). This new Dispatch (action) calls the original Dispatch and adds its own logic before and after the call. So now the structure of a middleware is clear:

  1. A middleware receiverstoreAs an argument, a function is returned
  2. The function returned accepts the olddispatchFunction as an argument, returns a new function
  3. The new function returned is newdispatchFunction, the inside of this function can be passed in two layersstoreAnd the olddispatchfunction

So to put it bluntly, middleware is about enhancing the capabilities of dispatches and replacing old dispatches with new ones. Isn’t that decorator mode? Enhancer is also a decorator pattern, passing in a createStore, adding some code before and after the createStore execution, and finally returning an enhanced createStore. As you can see, design patterns are really widespread in these excellent frameworks, and if you’re not familiar with decorator patterns, check out my previous post.

With this in mind, we can write applyMiddleware:

// Take the previous structure directly
function applyMiddleware(middleware) {
  function enhancer(createStore) {
    function newCreateStore(reducer) {
      const store = createStore(reducer);
      
      // Take middleware and run it into store
      // Get the first level function
      const func = middleware(store);
      
      // Deconstruct the original dispatch
      const { dispatch } = store;
      
      // Pass the original dispatch function to func execution
      // Get an enhanced dispatch
      const newDispatch = func(dispatch);
      
      // Replace the original dispatch with the enhanced newDispatch on return
      return{... store,dispatch: newDispatch}
    }
    
    return newCreateStore;
  }
  
  return enhancer;
}
Copy the code

As usual, we used our own applyMiddleware instead of the old one, and it ran just as well

Support multiplemiddleware

One thing our applyMiddleware doesn’t have is the ability to support more than one middleware, such as this:

applyMiddleware(
  rafScheduler,
  timeoutScheduler,
  thunk,
  vanillaPromise,
  readyStatePromise,
  logger,
  crashReporter
)
Copy the code

To support this, we can simply return newdispatches with incoming middleware and execute them one by one. Serial execution of multiple functions can be composed using the auxiliary function, which is defined as follows. Compose should return a method that wraps all of its methods.

function compose(. func){
  return funcs.reduce((a, b) = > (. args) = >a(b(... args))); }Copy the code

For example, we have three functions, all of which are methods for receiving and returning a new dispatch:

const fun1 = dispatch= > newDispatch1;
const fun2 = dispatch= > newDispatch2;
const fun3 = dispatch= > newDispatch3;
Copy the code

What is the order of execution when we use compose(fun1, fun2, fun3)?

// The first execution is actually executed
(func1, func2) => (. args) = >func1(fun2(... args))// This time the return value is the following, use a variable to store
const temp = (. args) = >func1(fun2(... args))// The next time we recycle, we actually execute
(temp, func3) => (. args) = >temp(func3(... args));// The return value is the following, the final return value, which is the function from func3 from right to left
// The previous return value will be used as the following argument
(. args) = >temp(func3(... args));// What happens if you pass dispatch as an argument
(dispatch) = > temp(func3(dispatch));

Func3 (dispatch) then returns newDispatch3, which is passed to Temp (newDispatch3)
(newDispatch3) = > func1(fun2(newDispatch3))

Fun2 (newDispatch3) with newDispatch3 returns newDispatch2
// Then func1(newDispatch2) gets newDispatch1
NewDispatch1 contains the logic of newDispatch3 and newDispatch2
Copy the code

More details on the compose principle can be found in my previous article.

So our code for supporting multiple Middleware looks like this:

// Parameters support multiple middleware
function applyMiddleware(. middlewares) {
  function enhancer(createStore) {
    function newCreateStore(reducer) {
      const store = createStore(reducer);
      
      // Multiple middleware, first deconstruct the structure of dispatch => newDispatch
      const chain = middlewares.map(middleware= > middleware(store));
      const { dispatch } = store;
      
      // Use compose to get a function that combines all newDispatches
      constnewDispatchGen = compose(... chain);// Execute this function to get newDispatch
      const newDispatch = newDispatchGen(dispatch);

      return{... store,dispatch: newDispatch}
    }
    
    return newCreateStore;
  }
  
  return enhancer;
}
Copy the code

Finally, we add a logger2 middleware to achieve the effect:

function logger2(store) {
  return function(next) {
    return function(action) {
      let result = next(action);
      console.log('logger2');
      return result
    }
  }
}

let store = createStore(reducer, applyMiddleware(logger, logger2));
Copy the code

You can see that Logger2 is printed, and you’re done.

Now we can also see why his middleware wraps several layers of functions:

Level 1: The purpose is to pass in the Store argument

Layer 2: The structure of layer 2 is Dispatch => newDispatch. Multiple middleware functions of this layer can compose to form a large dispatch => newDispatch

Layer 3: This layer is the final return value, which is the newDispatch, which is the enhanced Dispatch, where the real logic of the middleware resides.

ApplyMiddleware is now ready to use, and can be downloaded from this blog.

All the code for this article has been uploaded to GitHub, you can go to play:Github.com/dennis-jian…

conclusion

  1. Redux alone is just a state machine,storeIt stores all the statesstateTo change the state insidestateThat can only bedispatch action.
  2. For the outgoingactionNeed to usereducerTo deal with,reducerWill calculate the newstateTo replace the oldstate.
  3. subscribeMethod can register callback methods whendispatch actionWill execute the inside callback.
  4. Redux is basically a publish and subscribe model!
  5. Story also supportsenhancer.enhancerIt’s essentially a decorator pattern passed into the currentcreateStore, returns an enhancedcreateStore.
  6. Use ReduxapplyMiddlewareSupport middleware,applyMiddlewareThe return value ofenhancer.
  7. Redux’s middleware is also a decorator pattern, passed into the currentdispatch, returns an enhanceddispatch.
  8. Pure Redux doesn’t have a View layer, so it can be used in conjunction with various UI libraries, such asreact-reduxPlan to write your next essay by handreact-redux.

The resources

Official document: redux.js.org/

GitHub source: github.com/reduxjs/red…

At the end of this article, thank you for your precious time to read this article. If this article gives you a little help or inspiration, please do not spare your thumbs up and GitHub stars. Your support is the motivation of the author’s continuous creation.

Welcome to follow my public numberThe big front end of the attackThe first time to obtain high quality original ~

“Front-end Advanced Knowledge” series:Juejin. Cn/post / 684490…

“Front-end advanced knowledge” series article source code GitHub address:Github.com/dennis-jian…