Yesterday’s article hand-wrote a version of redux’s core source code, redux library in addition to data state management and an important piece of content that is middleware, today I still try to complete this part of the source code.

The middleware

In React, the data management process is one-way, that is, it is a one-way road from action distribution to publish and subscribe triggering rendering. If you want to add or change some logic in the middle, you need to find action or reducer to modify. Is there a more convenient way?

Middleware is a pluggable mechanism. If you want to extend something, such as adding logs and printing state before and after updates, you can simply install the logging middleware on Redux and remove it when you don’t want to use it.

There are many third-party middleware installations available, such as the logging middleware just mentioned: Redux-Logger, which is installed using NPM:

npm install redux-logger

The Redux package provides a way to load middleware: applyMiddleware. When creating the Store object, you can pass in a second argument, which is middleware:

import { createStore, applyMiddleware } from "redux";
import { reducer } from "./reducer";
import ReduxLogger from "redux-logger";
// Use applyMiddleware to load middleware
let store = createStore(reducer, applyMiddleware(ReduxLogger));
Copy the code

When the middleware is loaded, the corresponding functions are extended in dispatching actions. At this time, we write the redux program normally and print the state update log on the console when the dispatch method is executed:

This is an example of using middleware.

Analyze the principle of middleware

So how does middleware work? For example, the logging middleware is used to output state before and after the update of the state object, which must be implemented at the moment of dispatch. Let’s rewrite the redux library and add the “print log” function to the Dispatch method:

let temp = store.dispatch;// hold the old dispatch method
store.dispatch = function(action) {
  console.log("Old state:", store.getState());
  temp(action);// Execute the old dispatch method
  console.log("New state:", store.getState());
};
Copy the code

This enables “logging middleware”, but it is not possible to directly rewrite the Redux library. We need a common way to define middleware, and Redux provides one: applyMiddleware.

It’s as simple as passing the middleware that needs to be loaded into the applyMiddleware method:

applyMiddleware(ReduxLogger, ReduxThunk);

Handwritten applyMiddleware source code

ApplyMiddlware sends the middleware dispatch method and the original dispatch to the middleware.

var applyMiddleware = (middlewares) = > (createStore) => (reducer) = > {};
Copy the code

This is the applyMiddleware method, which is a three-level, higher-order function that uses currization to split multiple arguments into single-parameter higher-order functions so that each layer has only one argument, making it more flexible to block calls. It’s not easy to write it as an arrow function, so let’s write it as a normal function:

var applyMiddleware = function (middlewares){
  return function (createStore){
    return function (reducer){
      // Load the middleware here}}};Copy the code

As you can see from the function parameters, the three layers of functions pass in the middleware, createStore, and reducer functions, which are what we need to load a piece of middleware.

Our next goal is to “load” the middleware by overwriting Redux’s original Dispatch method with the dispatches provided by the middleware.

var applyMiddleware = function (middlewares) {
    return function (createStore) {
        return function (reducer) {
            let store = createStore(reducer);
            // Call the middleware and return the new dispatch method
            let newDispatch = middlewares(store)(store.dispatch);
            // Override the old dispatch method and return the warehouse object
            return {
                ...store,
                dispatch: newDispatch
            }
        }
    }
}
Copy the code

With the generic notation, we simulated a logging middleware implementation ourselves:

function reduxLogger(store) {
    return function (dispatch) {
        The //dispatch parameter is the original redux dispatch method
        return function (action) {
            // The function returned is the new method
            // applyMiddleware will eventually be passed to override the dispatch
            console.log('Before update:The ${JSON.stringify(store.getState())}`);
            dispatch(action);
            console.log('After update:The ${JSON.stringify(store.getState())}`); }}}Copy the code

Call our own methods to load middleware: applyMiddleware(reduxLogger); , the running effect is as follows:

Combined middleware

But that’s not all. Remember the official Redux library? ApplyMiddlewares supports passing multiple middleware, such as applyMiddlewares(MIDDLEware1,middleware2); Our current method does not support this writing method, the ultimate goal is to combine several middleware at a time into a whole, together load.

The onion model

The concept of the Onion model seems to have been developed in the Koa2 framework. It refers to the implementation mechanism of middleware, where multiple middleware are executed, the latter middleware is nested within the former middleware:

After one execution, the middleware will go in until the last execution is finished, and then out again, like peeling an onion.

Compose method

We also use the Onion model to write a combinatorial method for this purpose.

Create a new compose.js and create a compositing function:

/** * combine all middleware * @param {... any} middlewares */
function compose(. middlewares) {
    return function (. args) {}}Copy the code

My goal is to combine all the middleware into one function when calling the composition function, passing in multiple middleware:

var all = compose(middleware3, middleware2, middleware1);
all();When called, all middleware is executed in turn
Copy the code

Let’s do it.

This is the convergence idea of functional programming. ES6 already provides us with the Reduce function, which is most appropriate here:

/** * combine all middleware * @param {... any} middlewares */
function compose(. middlewares) {
  // Args is the first required middleware parameter
  return function (. args) {
    return middlewares.reduce((composed, current) = > {

      return function (. args) {
        // The execution result of the current middleware is the parameter of the previous middleware
        returncomposed(current(... args)); }}) (... args); }}Copy the code

Through the reduce function, the latter intermediate piece is set to the former middleware step by step, and the result of the latter middleware is the parameter of the former. In this way, a large function is finally returned, that is, the combination is completed.

Finally, it can be optimized to the form of arrow function, which is a bit higher:

function compose(. middlewares) {
  return (. args) = > middlewares.reduce((composed, current) = >(... args) => composed(current(... args)))(... args) }Copy the code

Complete middleware load

After Compose completes, the final step is to rewrite applyMiddlewares to compose all incoming middleware:

function applyMiddleware(. middlewares) {
    return function (createStore) {
        return function (reducer) {
            let store = createStore(reducer);
            // Multiple middleware are passed in at once, and the loop wraps one layer of functions
            let chain = middlewares.map(middleware= > {
                return middleware(store);
            });

            // Combine all middleware
            letnewDispatch = compose(... chain)(store.dispatch);// Override the old dispatch method
            return {
                ...store,
                dispatch: newDispatch
            }
        }
    }
}
Copy the code

At this point, the source code of the Redux library has been basically completed.

Multiple middleware runs as follows:

The tail

These two days from scratch handwritten redux library found that the amount of redux source code is not large but the logic is still very complex, clear redux process is the premise of reading and writing source code. However, middleware is one of the difficulties of redux library, mainly because the layer upon layer call relationship is very annoying. A good way is to compare the library source code with the middleware source code to analyze and clarify my thoughts. If I still have time, I will try to write a version of React-Redux library by hand, one is to improve my study and the other is to cope with the interview.