The story structure

I thought about implementing one myselfRedux? It’s not that hard,ReduxMainly because ofstore.reducer.actionNext, let’s try to build it step by stepRedux.

Step by step implementation of Redux

reducer

According to the introduction in the above figure, we know that Reducer is to process the relevant state according to the sent type and then return a new state. From this we get the following code:

// reducer.js
const init = {
    num: 0
}

export const reducer = (state = init, action) = > {
    switch(action.type) {
        case 'add': 
            return {
                ...state,
                num: state.num + 1
            }
        case 'low':
            return {
                ...state,
                num: state.num - 1
            }
        default:
            return init
    }

}
Copy the code

store

We implement the first step in the figure above, which is the store.js file. We first need to clear store file there are three main important function, respectively is the subscribe, dispatch, getState. Next, just paste the code and analyze it.

// store.js
import { reducer } from './reducer.js'

export const createStore = () = > {
    let currentState = { }
    let collect = []
    dispatch({})

    function getState() {
        return currentState
    }

    function dispatch(action) {
        currentState =  reducer(currentState, action)
        collect.forEach(tempFunc= > tempFunc())
    }

    function subscribe(tempFunc) {
    	if (fn instanceof Function) {
			collect.push(tempFunc)
		}
        return
    }
  
    return { getState, dispatch, subscribe }
}

Copy the code

We can see that the createStore function has a dispatch({}) line in addition to the three basic functions, which is actually to initialize redux. If no initialization is triggered in reducer, a NaN value will be obtained if related values are added or subtracted.

The Subscribe function is implemented in observer mode. When the user subscribes to the subscribe function on the page, after dispatch, all subscribe functions on the page are triggered. It’s a hassle. Let’s go to the code.

// index.js
import React from 'react'
import { createStore } from '.. /.. /store/store'
import { reducer } from '.. /.. /store/reducer'

const store = createStore(reducer)  
export class Roll extends React.Component {
    
    constructor(props) {
        super(props)
        this.state = {
            num:0}}componentWillMount() {
        store.subscribe(() = >this.setState({
            num: store.getState().num
        }))
    }
    lowNum() {
        store.dispatch({ type: 'low' })
        console.log('Store' has a value of ' + store.getState().num)
    }
    addNum() {
        store.dispatch({ type: 'add' })
        console.log('Store' has a value of ' + store.getState().num)
    }
    render() {
        return (
            <div style={{ textAlign:'center', paddingTop:'100px'}} >
                <button onClick={() = > this.lowNum() }>low</button>
                <div style={{ display: 'inline', padding:'0 10px'}} >{this.state.num}</div>
                <button onClick={() = > this.addNum() }>add</button>
            </div>)}}Copy the code

With the subscribe function:

Don’t addsubscribeFunction renderings:If you don’t, you’re actually updating itstoreThe state of the inside, butstoreIs not synchronized to the page and cannot trigger page updates.

The realization of the react – story

Instead of using Redux directly in React projects, we use React-Redux as a bridge between the two.

example

First let’s look at a simple way to use React-Redux.

// Provider pseudocode
ReactDOM.render(
	<Provider store={store}>
		<ChildComponent />
	</Provider>
)

/ / connent pseudo code
ChildComponent = connect(mapStateToProps, mapDispatchToProps)(ChildComponent)
Copy the code

Provider

ProviderEquivalent to a container component, a container can be nested with multiple layers of components, in effectProviderIt doesn’t do anything to the component inside, just let the component display properly, and it accepts onestoreParameters, it’s going to send this outsidestoreParameter passed tocontext, and then make the component the root node of the component tree, so that all its children are availablecontext.

// provider.js
import React from 'react'
import PropTypes from 'prop-types'

export class Provider extends React.Component {
    // Declare the Context object properties
    static childContextTypes = {
        store: PropTypes.object,
        children: PropTypes.object
    }
    // Returns a property in the Context object
    getChildContext = () = > {
        return {
            store: this.props.store
        }
    }

    render () {
        return (
            <div>{this.props.children}</div>)}}Copy the code

Connect

The connect function actually takes a component as an argument and returns a new component, which is called HOC. It takes two parameters, mapStateToProps and mapDispatchToProps, in addition to one component. These are the props passed into the component that need to be passed back to the original component as-is by the connect higher-order component. We have an overview of the process and can implement it simply:

import React from 'react'
import PropTypes from 'prop-types'

export function connect(mapStateToProps, mapDispatchToProps) {
    // 1. Pass in state and Dispatch objects
  return function(WrappedCompment)  {
      // 2. Receive the incoming component
    class Connect extends React.Component {
        constructor() {
            super(a)this.state = {
                // 3. Consolidate all props on one object for easy writing
                mapStateAndDispatchProps: {}}}static contextTypes = {
            // 4. Get store in context
            store: PropTypes.object
        }

        componentDidMount() {
            const { store } = this.context
            // 5. Update and merge several incoming objects
            this.mergeAndUpdateProps()
            store.subscribe(() = > {
                this.mergeAndUpdateProps()
            })
        }

        mergeAndUpdateProps() {
            const { store } = this.context
            let tempState = mapStateToProps ? mapStateToProps(store.getState(), this.props) : {}
            let tempDispatch = mapDispatchToProps ? mapDispatchToProps(store.dispatch, this.props) : {}
            this.setState({ 
                mapStateAndDispatchProps: {... tempState, ... tempDispatch, ... this.props } }) }render() {
            // Put all the props passed into the previous component
            return <WrappedCompment {. this.state.mapStateAndDispatchProps} / >}}// Returns the new component
    return Connect
}
}
Copy the code

Implementation effect

Plug into the Roll component and test it:

// Roll.js
import React from 'react'
import { connect } from '.. /.. /store/connect'

const mapStateToProps = state= > {  
    return {      
        num: state.num  
    }
}

const mapDispatchToProps = dispatch= > {  
    return {      
        addNum: () = > {          
            dispatch({type: 'add'})},lowNum: () = > {
            dispatch({type: 'low'})}}}class Roll extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return (
            <div style={{ textAlign:'center', paddingTop:'100px'}} >
                <button onClick={() = > this.props.lowNum() }>low</button>
                <div style={{ display: 'inline', padding:'0 10px'}} >{this.props.num}</div>
                <button onClick={() = > this.props.addNum() }>add</button>
            </div>)}}export default connect(mapStateToProps, mapDispatchToProps)(Roll)
Copy the code

Final result:

Redux Middleware

You’ve probably used some of redux’s middleware, such as Redux-Thunk, Redux-Saga, redux-Logger, etc., but how is it implemented? Let’s do it one by one.

Why do you need middleware in the first place? Suppose we now have a scenario where we need to print the records of each dispatch. It is easy to think of printing after dispatch:

function dispatchAndPrint(store, dispatch) {
	dispatch({type: 'add'})
	console.log('newState:', store.getState())
}
Copy the code

But now there is a requirement to continue with the error we caught at dispatch, so what do we need to write:

function dispatchAndCatch(store, dispatch) {
	try {
		dispatch({type: 'add'})}catch(e) {
		console.error('dispatch error: ', err)  
		throw e
	}
}
Copy the code

So if we actually write more and more dispatches as these requirements grow, we can actually extract this dispatch step:

let next = store.dispatch
store.dispatch = function dispatchAndPrint(store) {
	next({type: 'add'})
	console.log('newState:', store.getState())
}

store.dispatch = function dispatchAndCatch(store, dispatch) {
	try {
		next({type: 'add'})}catch(e) {
		console.error('dispatch error: ', err)  
		throw e
	}
}
Copy the code

applyMiddleware

When we use middleware in Redux, we use applyMiddleware. ApplyMiddleware actually does the same thing as the example above. You can think of it as applyMiddleware getting a dispatch, We then modify the Dispatch in our middleware, depending on our middleware. We can implement a simple version of the applyMiddleware function for this.

const applyMiddleware = function(store, middleware){
  let next = store.dispatch;
  store.dispatch = middleware(store)(next);
}
applyMiddleware(dispatchAndPrint)
Copy the code

Chain calls to multiple middleware

We didn’t actually use applyMiddleware to mean only one middleware at a time, but how do we use multiple middleware?

We can pass in the dispatch returned by the previous middleware as the next function of the next middleware, and we can curryize the two functions:

const dispatchAndPrint = store= > next= > action= > {
	console.log('newState:', store.getState())
	return next(action)
}

const dispatchAndCatch = store= > next= > action= > {
	try {
		next(action)
	} catch(e) {
		console.error('dispatch error: ', err)  
		throw e
	}
}
Copy the code

Write applyMiddleware:

function applyMiddleware(store, middlewares) {
	// Shallow copy to prevent reverse from affecting the middleware
	middlewares = middlewares.slice() 
	If the array is not flipped, the function inserted first will be executed last in the innermost layer
	middlewares.reverse() 
	
	let dispatch = store.dispatch
	middlewares.map((middleware) = > {
		dispatch = middleware(store)(dispatch)
	})
	return { ...store, dispatch }
}
Copy the code

Middlewares is an array of middleware. We reverse the middlewares array because every time our middleware function just returns a new dispatch function to the next middleware, What we end up with is the function returned by the last middleware wrapped with dispatch, and if reversed, the last middleware will execute first and then push forward to execute the first middleware.

Go to the applyMiddleware source code

Of course, instead of reversing the middleware array as we do, we write the following:

function applyMiddleware(. middlewares) {
  return (createStore) = > (reducer, preloadedState, enhancer) = > {
    var store = createStore(reducer, preloadedState, enhancer);
    var dispatch = store.dispatch;
    var chain = [];

    var 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

Compose function:

function compose(. funcs) {
  if (funcs.length === 0) {
    return arg
  }

  if (funcs.length === 1) {
    // Just execute a function, execute the function, and return the result
    return funcs[0]}// When multiple functions are executed, reduce is used to recursively process these functions
  return funcs.reduce((a, b) = > (. args: any) = >a(b(... args)))Copy the code

The source code for applyMiddleware actually uses the compose function to pass the return value of the last middleware to the next middleware as a parameter, thus enabling middleware to cascade.

For example, the compose function is composed by c(b(a(… Args))), the execution order is a->b-> C.

conclusion

Perhaps you see behind the story – thunk source when I may feel this library why such a simple so a few lines of code, but in fact it is not necessary to surprise, because even the story is not very complicated, but contains JS programming thought is worth learning, such as the function of curry, functional programming, decorator, and so on knowledge.

Information:

8 k word | Redux/react – story/Redux middleware design implementation

Principles of Redux middleware

Redux Tutorial ii: Middleware and Asynchronous Operations

Currization of JavaScript functions

Code composition (compose)