This is the second part of A series on how to Make Wheels. We will write A small and complete Redux from scratch. This series of articles will analyze the source code of some classic wheels and implement them from scratch. Dom-diff, Webpack, Babel, KAO, Express, Async /await, jquery, Lodash, RequireJS, Lib-flexible and other front-end classic wheel implementation, each chapter source code is hosted on Github, Learn to Make Wheels Together (1) : Start from scratch Write A Promise that Promises/A+ Promises learn to Make Wheels Together (2) : Start from Scratch Write A Redux Learn how to Build Wheels at Github

preface

Redux is a JavaScript state container that provides predictable state management. In this article, we will introduce in detail the principles of Redux’s five core methods: createStore, applyMiddleware, bindActionCreators, combineReducers, and Compose. Finally, we will package a small and complete Redux library. Then we will introduce the realization principle of redux-Logger, Redux-Thunk, redux-Promise and other middleware commonly used with Redux.

This article will provide a brief overview of what Redux is and how to use the core methods of Redux. It is recommended that you learn the basics first if you have not already used Redux.

Redux Redux Redux Redux Redux Redux Redux Redux

All the code in this article is in github code repository, you can click here to view the code in this article, also welcome everyone star~

start

createStore

First, let’s look at a basic scenario using Redux:

functionreducer(state, Subscribe (() => renderApp(store.getstate ()))) // Register state change callback renderApp(store.getState()) // initialize page store.dispatch(xxxAction) // issue actionCopy the code

The above code is a basic scenario using Redux. First define a reducer, then use this reducer to generate a store, register the callback function to be executed when the state changes on the store, and then use the initial state to render the page. When there are operations on the page, The action and the old state are evaluated by the Reducer to generate a new state. The state changes and the callback function is triggered to re-render the page using the new state. This simple scenario covers the entire Redux workflow, as shown in the figure below:


function createStore(reducer) {
    letState = null // Used to store global statusletListeners = [] // Listeners => const subscribe = (listener) => {listeners.push(listener)} const Const dispatch = (action) => {// To receive an action and use reducer, Calculate the latest state from the old state and action, then iterate through the array of callback functions, State = reducer(state, Action) // Generate new state listeners. ForEach ((Listener) => Listener ()) // execute callback} dispatch({}) // initialize global statereturn{getState, dispatch, subscribe} // Returns a store object with three methods for external use}Copy the code

It’s actually not that complicated to implement this method

  1. First, we define two variables: state and Listeners. State is used to store the global state and listeners are used to store the array of callbacks that have changed.
  2. Then we define three methods subscribe, getState, and Dispatch. Subscribe is used to register the callback function, getState is used to get the latest state, and Dispatch is used to receive an action and use reducer to calculate the latest state based on the old state and action, and then iterate through the array of callback functions to execute the callback.
  3. When createStore is called, dispatch({}) is executed to generate an initial state using reducer, and then a store object is returned, on which getState, Dispatch, subscribe are mounted for external invocation

After the above three steps, we implemented a simple createStore method.

combineReducers

When we develop slightly larger projects, there are usually multiple reducers, and we will generally establish a Reducers folder, which stores all reducer used in the project. Then use a combineReducers method to merge all reducer into a pass to createStore method.

import userInfoReducer from './userinfo.js'
import bannerDataReducer from './banner.js'
import recordReducer from './record.js'
import clientInfoReducer from './clicentInfo.js'

const rootReducer = combineReducers({
    userInfoReducer,
    bannerDataReducer,
    recordReducer,
    clientInfoReducer
})

const store = createStore(rootReducer)
Copy the code

Next, we will implement the combineReducers method together:

const combineReducers = reducers => (state = {}, action) => {
    let currentState = {};
    for (let key in reducers) {
        currentState[key] = reducers[key](state[key], action);
    }
    return currentState;
};
Copy the code
  1. First, combineReducers receives a Reducer set and returns a merged reducer function. Therefore, the returned function input is still the same as normal Reducer, receiving state and action and returning the new state.
  2. Then declare a currentState object to store the global state, iterate through the Reducers array, and use the Reducer function to generate the corresponding state object and mount it to currentState. For example, two reducers were passed in as reducers{userInfoReducer,bannerDataReducer}UserInfoReducer state would look like this:{userId: 1, name: "* *"}And the state in the bannerData collector was originally{pictureId:1,pictureUrl:"http://abc.com/1.jpg"}The merged currentState changes to
{
    userInfoReducer: {
        userId: 1,
        name: "Zhang"
    },
    bannerDataReducer: {
        pictureId: 1,
        pictureUrl: "http://abc.com/1.jpg"}}Copy the code

Here we implement the second method combineReducers.

bindActionCreators

Next up is the bindActionCreators method, a helper method provided by Redux that allows you to call the Action as a method. At the same time, the corresponding action is automatically dispatched. It takes two arguments, the first taking an Action Creator and the second taking a Dispatch function, provided by the Store instance.

So let’s say we have a TodoActionCreators

export function addTodo(text) {
    return {
      type: 'ADD_TODO',
      text
    };
}
export function removeTodo(id) {
   return {
     type: 'REMOVE_TODO',
     id
   };
}
Copy the code

We needed to use it like this before:

import * as TodoActionCreators from './TodoActionCreators';

let addReadAction = TodoActionCreators.addTodo('read');
dispatch(addReadAction);

let addEatAction = TodoActionCreators.addTodo('eat');
dispatch(addEatAction);

let removeEatAction = TodoActionCreators.removeTodo('read');
dispatch(removeEatAction);
Copy the code

Now all it takes is this:

import * as TodoActionCreators from './TodoActionCreators';
let TodoAction = bindActionCreators(TodoActionCreators, dispatch);

TodoAction.addTodo('read')
TodoAction.addTodo('eat')
TodoAction.removeTodo('read')
Copy the code

Okay, so how to use it, let’s implement this method, okay

function bindActionCreator(actions, dispatch) {
    let newActions = {};
    for (let key in actions) {
        newActions[key] = () => dispatch(actions[key].apply(null, arguments));
    }
    return newActions;
}
Copy the code

The implementation of this method is not difficult. Each action in the ActionCreators app uses a function to wrap the Dispatch behavior and mount the function to an object to expose it. When this function is called externally, the corresponding action will be automatically dispatched. The implementation of this method also takes advantage of closures.

This method is often used with react-Redux, but will be discussed when we talk about the implementation principles of React-Redux.

compose

Finally, there are two methods: compose and applyMiddleware, both of which can be used with Redux middleware. The compose method is a redux helper method that assembles a bunch of functions into a new one. It is executed from the back to the front, with the execution result of the latter function as an argument to the execution of the previous function.

Let’s say we have functions like this:

function add1(str) {
    return str + 1
}

function add2(str) {
    return str + 2
}

function add3(str) {
    return str + 3
}
Copy the code

If we want to execute functions in sequence and pass the results to the next layer, we write it layer by layer as follows: Let newstr = add3(add2(add1(” ABC “))) //”abc123”

let newaddfun = compose(add3, add2, add1);
let newstr = newaddfun("abc") / /"abc123"
Copy the code

So how is compose implemented internally?

functioncompose(... funcs) {returnfuncs.reduce((a, b) => (... args) => a(b(... args))); }Copy the code

The core code is just one sentence, which uses the reduce method to elegantly convert a series of functions to add3(add2(add1(… When call compose(add3, add2, add1), funcs is add3, add2, add1, a is add3 and b is add2 for the first time, the expansion looks something like this: (add3, add2) =>(… args)=>add3(add2(… Args), passing add3, add2, returns a function like this (… args)=>add3(add2(… Args), and reduce continues, with a being the function returned from the previous step on the second entry (… args)=>add3(add2(… Args)), b is add1, so execute to a(b(… The args))), b (… Args), passed as arguments to function a, becomes this form :(… args)=>add3(add2(add1(… Args))), isn’t that clever?

applyMiddleware

Finally, let’s look at applyMiddleware, the last method we use in our Redux project when we use middleware:

import thunk from 'redux-thunk'
import logger from 'redux-logger'const middleware = [thunk, logger] const store = createStore(rootReducer, applyMiddleware(... middleware))Copy the code

We used thunk and Logger, passing a new parameter applyMiddleware(… Middleware), which tells Redux what middleware we want to use, so we’ll modify the createStore method to support passing middleware parameters.

functionCreateStore (Reducer, enhancer) {if the middleware function is passed in, use middleware to enhance the createStore methodif (typeof enhancer === 'function') {
        return enhancer(createStore)(reducer)
    }
    let state = null
    const listeners = []
    const subscribe = (listener) => {
        listeners.push(listener)
    }
    const getState = () => state
    const dispatch = (action) => {
        state = reducer(state, action)
        listeners.forEach((listener) => listener())
    }
    dispatch({})
    return { getState, dispatch, subscribe }
}
Copy the code

Then take redux-Logger middleware as an example to analyze the implementation of REdux middleware. First of all, we can think about what to do if we want to achieve logger functions without logger middleware.

let store = createStore(reducer);
let dispatch = store.dispatch;
store.dispatch = function (action) {
  console.log(store.getState());
  dispatch(action);
  console.log(store.getState())
};
Copy the code

We can wrap a layer of functions around the original dispatch method, so that the log is printed before and after the real dispatch, and the wrapped dispatch function is called when the call is made. In fact, this is the idea of the principle of Redux middleware: Replace store dispatch with a new function that is enhanced but still has the dispach function.

So how does the applyMiddleware method tweak Dispatch to enhance its functionality? Let’s start with a simple version. If we only have one middleware, how do we implement the applyMiddleware method?

function applyMiddleware(middleware) {
    return function a1(createStore) {
        return functionA2 (reducer) {// Reduce original dispatch const store = createStore(reducer)letConst middlewareAPI = {getState: store.getState, dispatch: (action) => dispatch(action) }letMid = Middleware (middlewareAPI) Dispatch = mid(store.dispatch) // Overwrite store with the wrapped dispatch. Dispatch returns a new store objectreturn{... Store, dispatch}}}} // Middlewarelet logger = function({ dispatch, getState }) {
        return function l1(next) {
            return functionl2(action) { console.log(getState()); Next (action) console.log(getState())}}} // Reducer functionfunction reducer(state, action) {
    if(! state) state = { count: 0 } console.log(action) switch (action.type) {case 'add':
            letobj = {... state, count: ++state.count }return obj;
        case 'sub':
            return{... state, count: --state.count } default:return state
    }
}

const store = createStore(reducer, applyMiddleware(logger))
Copy the code
  1. First we define an applyMiddleware method that takes middleware as an argument. A Logger middleware function is then defined that receives the Dispatch and getState methods for internal use. Both of these functions are implemented using higher-order functions in the Redux source code. In keeping with the source code, they are also implemented using higher-order functions, but for ease of understanding, use the named function function instead of the anonymous arrow function to make it clearer.

  2. Const store = createStore(Reducer,applyMiddleware(Logger)),applyMiddleware(Logger) It then returns a function a1 that receives the createStore method and passes a1 as the second argument to the createStore method. Since the second argument is passed, the createStore method actually executes this code:

if (typeof enhancer === 'function') {
    return enhancer(createStore)(reducer)
}
Copy the code

When return enhancer(createStore)(reducer) is executed, a1 (createStore)(reducer) is executed. When A1 (createStore) is executed, A2 is returned. The last return is the result of a2(reducer).

  1. Then, let’s look at what is done inside A2. I define three stages for this function. The first stage is the raw Dispatch stage, which executes the createStore(Reducer) method and produces the original dispatch method.

  2. First, we define the middlewareAPI to be used by middleware functions. Here, getState directly uses Store. getState, while Dispatch uses a layer of functions. (Action)=> Dispatch (action), why? Because the dispatch method we will eventually use for middleware must be the dispatch method wrapped by various middleware, not the original method, so we set the dispatch method as a variable here. We pass the middlewareAPI to middleware, which returns a function mid(l1 in Logger) that takes a next method as an argument, and when we call Dispatch = mid(store.dispatch), If we pass in store.dispatch as the next method and return l2 as the new dispatch, we can see that the new dispatch method does the same thing as we did above:

function l2(action) {
    console.log(getState());
    next(action)
    console.log(getState())
}
Copy the code

Both receive an action, print the log, execute the original Dispatch method to send an action, and then print the log.

  1. Finally, we get to the third stage: overriding the store.dispatch method with the wrapped dispatch and returning the new Store object.

  2. At this point, when we execute store.dispatch({type:add}) outside, we actually execute the wrapped dispatch method, so the Logger middleware takes effect, as shown in the figure, the latest state is printed before and after the actual dispatch:

import compose from './compose';

functionapplyMiddleware(... middlewares) {return function a1(createStore) {
        return function a2(reducer) {
            const store = createStore(reducer)
            let dispatch = store.dispatch
            letchain = [] const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch)return {
                ...store,
                dispatch
            }
        }
    }
}

let loggerone = function({ dispatch, getState }) {
    return function loggerOneOut(next) {
        return function loggerOneIn(action) {
            console.log("loggerone:", getState());
            next(action)
            console.log("loggerone:", getState())
        }

    }
}
let loggertwo = function({ dispatch, getState }) {
    return function loggerTwoOut(next) {
        return function loggerTwoIn(action) {
            console.log("loggertwo:", getState());
            next(action)
            console.log("loggertwo:", getState())
        }
    }
}
const store = createStore(reducer, applyMiddleware([loggertwo, loggerone]))
Copy the code
  1. First, when the applyMiddleware method is called, you pass in an array of middleware instead of a piece of middleware.

  2. We then maintain a chain array in the applyMiddleware method that stores the middleware chain.

  3. When you perform to chain = middlewares. The map (middleware = > middleware (middlewareAPI)), the chain store inside [loggerTwoOut loggerOneOut].

  4. The next step is to use the compose method we talked about earlier, dispatch=compose(… Dispatch =loggerTwoOut(loggerOneOut(store.dispatch)) LoggerTwoOut (loggerOneOut(store.dispatch)). When loggerOneOut(store.dispatch) is executed, loggerOneIn is returned. Use the Store. dispatch method as the next method in loggerOneIn. The function now looks like this: loggerTwoOut(loggerOneIn). When this sentence is executed, the loggerTwoIn function is returned and loggerOneIn is the next method in the loggerTwoIn method. Finally, assign dispatch =loggerTwoIn.

  5. Externally when we call store.dispatch({type:add}), we actually execute loggerTwoIn({type:add}), so console.log(“loggertwo:”, getState()), Next (action) executes loggerOneIn(Action), inside loggerOneIn, so console.log(” loggerOne :”,getState()) is executed; Next (Action), which actually executes the original store.dispatch method, will actually submit the action, and then continue executing, console.log(“loggerone:”,getState()), The loggerOneIn execution is then completed, and the execution is returned to the loggerTwoIn layer, and the loggerTwoIn execution continues, console.log(” LoggerTwo :”, getState()), and the end.

That’s it for the Applymiddleware method. Let’s take a look at redux’s official source code implementation:

functionapplyMiddleware(... middlewares) {return (createStore) => (reducer, preloadedState, enhancer) => {
        const store = createStore(reducer, preloadedState, enhancer)
        let dispatch = store.dispatch
        letchain = [] const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch)return {
            ...store,
            dispatch
        }
    }
}
Copy the code

The applyMiddleware method does everything except preloadedState when the front and back end are homogeneous. To this we redux in all methods are implemented again, of course, we implement only the most core and most commonly used parts of each method, and did not redux source code word for word to translate. Because I think for the source code learning should seize the main line, learn the core code and flash point in the source code, if interested in redux other features, you can read the official source code to learn.

Commonly used middleware redux-Logger, redux-thunk, redux-promise

Next, we’ll implement three middleware pieces commonly used by Redux

redux-logger

let logger = function({ dispatch, getState }) {
        return function(next) {
            return function(action) {
                console.log(getState());
                next(action)
                console.log(getState())
            }
        }
    }
Copy the code

We already talked about this in applyMiddleware, so I won’t talk about it anymore.

redux-thunk

Redux-thunk can be used to submit actions asynchronously. Redux-thunk can be used to submit actions asynchronously

const fetchPosts = postTitle => (dispatch, getState) => {
    dispatch(requestPosts(postTitle));
    return fetch(`/some/API/${postTitle}.json`)
        .then(response => response.json())
        .then(json => dispatch(receivePosts(postTitle, json)));
};
store.dispatch(fetchPosts('reactjs'))
Copy the code

FetchPosts (‘reactjs’) returns a function, while redux’s dispatch method cannot accept a function. The official redux source code explicitly states that an action must be a pure object, and middleware is required to process asynchronous actions.

function dispatch(action) {
    if(! isPlainObject(action)) { throw new Error('Actions must be plain objects. ' +
            'Use custom middleware for async actions.')}... }Copy the code

So what does Redux-Thunk do to pass functions to Dispatch?

let thunk = function({ getState, dispatch }) {
    return function(next) {
        return function(action) {
            if (typeof action == 'function') {
                action(dispatch, getState);
            } else{ next(action); }}}}Copy the code

The thunk middleware internally determines that if a function is passed in, it executes it, leaving it to the next middleware. For example, when executing store.dispatch(fetchPosts(‘reactjs’)), it passes a message to the dispatch Function:

postTitle => (dispatch, getState) => {
    dispatch(requestPosts(postTitle));
    return fetch(`/some/API/${postTitle}.json`)
        .then(response => response.json())
        .then(json => dispatch(receivePosts(postTitle, json)));
};
Copy the code

The Thunk middleware finds a function and executes it, issuing an Action (requestPosts(postTitle)) and then performing an asynchronous operation. After receiving the result, convert it to JSON format and issue an Action (receivePosts(postTitle, JSON)). Both actions are normal objects, so when dispatch goes else {next(Action); } this branch, continue execution. This solves the problem of dispatch not accepting functions.

redux-promise

Redux-promise middleware can now support incoming functions, and with redux-Promise we can make it support incoming promises. There are two ways to use this middleware: write 1, return a promise object.

const fetchPosts =
    (dispatch, postTitle) => new Promise(function(resolve, reject) {
        dispatch(requestPosts(postTitle));
        return fetch(`/some/API/${postTitle}.json`)
            .then(response => {
                type: 'FETCH_POSTS',
                payload: response.json()
            });
    });
Copy the code

The payload of an Action object is a Promise object. This requires importing the createAction method from redux and writing it like this.

import { createAction } from 'redux-actions';

class AsyncApp extends Component {
    componentDidMount() {const {dispatch, selectedPost} = this. Props (requestPosts(selectedPost)); // Dispatch asynchronous Action dispatch(createAction('FETCH_POSTS',
            fetch(`/some/API/${postTitle}.json`) .then(response => response.json()) )); }}Copy the code

Let’s implement the Redux-Promise middleware:

let promise = function({ getState, dispatch }) {
        return function(next) {
            return function(action) {
                if (action.then) {
                    action.then(dispatch);
                } else if(action.payload && action.payload.then) { action.payload.then(payload => dispatch({... action, payload }), payload => dispatch({... action, payload })); }else{ next(action); }}}}Copy the code

Redux-thunk we implement redux-thunk by saying that if function is passed, it will execute, otherwise next(action) will continue; Redux-promise Similarly, when an action or action payload has a THEN method on it, we consider it a promise object and execute it in the promise’s THEN until the action submitted by Dispatch has no THEN parties Next (Action) will be implemented by the next middleware.

The last

This article introduces the principles of Redux’s five methods, createStore, applyMiddleware, bindActionCreators, combineReducers, and Compose, and provides a compact and complete Redux library. At the same time, the realization principle of three commonly used middleware redux-Logger, Redux-Thunk and Redux-Promise in Redux is briefly introduced. All the code in this paper has a code warehouse in Github, you can click to view the source code of this article.

React-redux and Redux-Saga are classic wheels related to Redux. Since this article is very long, the implementation of these two wheels will be put into the follow-up learning To Build Wheels series. Please pay attention to them