If you and I have the same question, it shows that you do not understand the principle of Redux middleware. Let’s first talk about what is the function of Redux middleware. Take a look at the source code for Redux’s middleware and applyMiddleware

To view the demo

View the source code, welcome star

Higher-order functions

A high-order function is a function that satisfies one of the following two conditions:

  • Functions can be arguments
  • A function can be a return value

The setTimeout, Map, filter and reduce functions we usually use belong to higher-order functions. Of course, the Currization of functions we will talk about today is also an application of higher-order functions

Currization of a function

What is the Currization of a function? Those who have read the JS elevation book should know that there is a chapter dedicated to advanced JS techniques, which for the function of the currization is described like this:

It is used to create functions that have one or more parameters set. The basic use of currization of functions is the same as function binding: a closure is used to return a function. The difference is that when a function is called, the returned function also needs to set some of the parameters passed in

It’s a little confusing, isn’t it? Let’s do an example

const add = (num1, num2) => {
    return num1 + num2
}

const sum = add(1, 2)
Copy the code

Add is a function that returns the sum of two arguments, and if you were to make a Currified change to add, it would look like this

const curryAdd = (num1) => {
    return (num2) => {
        return num1 + num2
    }
}
const sum = curryAdd(1)(2)
Copy the code

It is more commonly written as follows:

const curry = (fn, ... initArgs) => {let finalArgs = [...initArgs]
    return(... otherArgs) => { finalArgs = [...finalArgs, ...otherArgs]if (otherArgs.length === 0) {
            return fn.apply(this, finalArgs)
        } else {
            returncurry.call(this, fn, ... finalArgs) } } }Copy the code

We are modifying our Add to accept any parameter

const add = (... args) => args.reduce((a, b) => a + b)Copy the code

Then use the one we wrote above, Curry, to curryize add

const curryAdd = curry(add)

curryAdd(1)
curryAdd(2, 5)
curryAdd(3, 10)
curryAdd(4)
const sum = curryAdd() // 25
Copy the code

Note that we must finally call curryAdd() to return the result. You can also modify Curry to return the result when the number of arguments passed by fn reaches the number specified by fn

In short, the corrification of a function is to convert a multi-parameter function into a single-parameter function. The single-parameter here does not just refer to one parameter. My understanding is parameter segmentation

PS: For sensitive students, this is very similar to the implementation of the BIND function in ES5. Let’s start with a little bit of bind that I implemented myself

Function.prototype.bind = function(context, ... initArgs) { const fn = thislet args = [...initArgs]
    return function(... otherArgs) { args = [...args, ...otherArgs]returnfn.call(context, ... args) } } var obj = { name:'monkeyliu',
	getName: function() {
		console.log(this.name)
	}
}

var getName = obj.getName
getName.bind(obj)() // monkeyliu
Copy the code

Elevation says this about them both:

ES5’s bind method also implements currification of functions. Using bind or Curry depends on whether an Object response is required. Both can be used to create complex algorithms and functions, and neither should be overused, since each function incurs additional overhead

Redux middleware

What is Redux middleware? My understanding is to allow users to add their own code before and after Dispatch (Action), which may not be very accurate, but is the best way to understand it for those of you new to Redux middleware

I’ll use an example of logging and printing execution times to help you go from analyzing a problem to solving a problem by building Middleware

When we dispatch an action, we want to record the current value of the action and the state value after the change. What do we do?

Manual record

The dumbest way to do this is to print the current action before dispatch and the changed state after dispatch. Your code might look something like this

const action = { type: 'increase' }
console.log('dispatching:', action)
store.dispatch(action)
console.log('next state:', store.getState())
Copy the code

This is the general people will think of the way, simple, but poor universality, if we have to log in multiple places, the above code will be written multiple times

Encapsulation Dispatch

To reuse our code, we’ll try to encapsulate the above code as a function

const dispatchAndLog = action => {
    console.log('dispatching:', action)
    store.dispatch(action)
    console.log('next state:', store.getState())
}
Copy the code

But all this does is reduce the amount of code we need to use, and we still have to introduce this method every time we need to use it

Retrofit native Dispatches

Override store. Dispatch directly so we don’t have to import dispatchAndLog every time, which is called monkeypatch on the Internet, and your code might look something like this

const next = store.dispatch
store.dispatch = action => {
    console.log('dispatching:', action)
    next(action)
    console.log('next state:', store.getState())
}
Copy the code

This is enough for one change, multiple uses, and it works for what we want, but it’s not over yet.

Record execution time

When we need to log the execution time before and after dispatch in addition to logging, we need to build another middleware and execute the two in turn, your code might look something like this

const logger = store => {
    const next = store.dispatch
    store.dispatch = action => {
        console.log('dispatching:', action)
        next(action)
        console.log('next state:'. store.getState()) } } const date = store => { const next = store.dispatch store.dispatch = action => { const date1 = Date.now() console.log('date1:', date1)
        next(action)
        const date2 = Date.now()
        console.log('date2:', date2)
    }
}

logger(store)
date(store)
Copy the code

But in this case, the print would look like this:

date1: 
dispatching: 
next  state: 
date2: 
Copy the code

The middleware outputs the results in reverse order of the middleware execution

Use higher order functions

What if instead of overwriting store.dispatch in Logger and Date, we use higher-order functions to return a new function?

const logger = store => {
    const next = store.dispatch
    return action => {
        console.log('dispatching:', action)
        next(action)
        console.log('next state:', store.getState())
    }
}

const date = store => {
    const next = store.dispatch
    return action => {
        const date1 = Date.now()
        console.log('date1:', date1)
        next(action)
        const date2 = Date.now()
        console.log('date2:', date2)
    }
}
Copy the code

Then we need to create a function to receive Logger and date, which we loop through and assign to store. Dispatch, which is a prototype of applyMiddleware

const applyMiddlewareByMonkeypatching = (store, middlewares) => {
    middlewares.reverse()
    middlewares.map(middleware => {
        store.dispatch = middleware(store)
    })
}
Copy the code

Then we can apply our middleware like this

applyMiddlewareByMonkeypatching(store, [logger, date])
Copy the code

But it still belongs to the monkey show play, its implementation details, we will just hide inside applyMiddlewareByMonkeypatching

Currization of associative functions

An important feature of the middleware is that the latter middleware can use the store.dispatch packaged by the former middleware, which can be realized through the currization of functions. We have transformed the former logger and date

const logger = store => next => action => {
    console.log('dispatching:', action)
    next(action)
    console.log('next state:', store.getState())
}

const date = store => next => action => {
    const date1 = Date.now()
    console.log('date1:', date1)
    next(action)
    const date2 = Date.now()
    console.log('date2:', date2)
}
Copy the code

Redux’s middleware is written this way, with next being the function returned by the previous middleware and returning a new function as the input value for the next middleware, Next

Therefore our applyMiddlewareByMonkeypatching also needs to be turned down, we named it applyMiddleware

const applyMiddleware = (store, middlewares) => {
    middlewares.reverse()
    let dispatch = store.dispatch
    middlewares.map(middleware => {
        dispatch = middleware(store)(dispatch)
    })
    return { ...store, dispatch }
}
Copy the code

We can use it like this

let store = createStore(reducer)

store = applyMiddleware(store, [logger, date])
Copy the code

ApplyMiddleware is a bit different from Redux’s applyMiddleware, but it’s a bit different from the native applyMiddleware source code

ApplyMiddleware source

Go directly to the source code for applyMiddleware

export default functionapplyMiddleware(... middlewares) {returncreateStore => (... args) => { const store = createStore(... args)let dispatch = () => {
      throw new Error(
        `Dispatching whileconstructing your middleware is not allowed. ` + `Other middleware would not be applied to this dispatch.` ) } const middlewareAPI = { getState: store.getState, dispatch: (... args) => dispatch(... args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch)return {
      ...store,
      dispatch
    }
  }
}
Copy the code

Native applyMiddleware is the second parameter in createStore, and we also post the core code for createStore and analyze it together

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

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

When applyMiddleware is passed in, enhancer(createStore)(Reducer, preloadedState) returns a store object. We execute it and return a function that takes a createStore argument, and then we go ahead and enhancer(createStore) returns a function, Finally we implement enhancer(createStore)(Reducer, preloadedState). What do we do in this reducer?

const store = createStore(... args)Copy the code

First create a store object using the Reducer and preloadedState

let dispatch = () => {
  throw new Error(
    `Dispatching while constructing your middleware is not allowed. ` +
      `Other middleware would not be applied to this dispatch.`
  )
}
Copy the code

Dispath should not be called while building middleware, otherwise an exception will be thrown

const middlewareAPI = { getState: store.getState, dispatch: (... args) => dispatch(... args) }Copy the code

The definition middlewareAPI object contains two properties, getState and Dispatch, and is used as the middleware input parameter store

const chain = middlewares.map(middleware => middleware(middlewareAPI))
Copy the code

Chain is an array, and each entry in the array is a function that takes next and returns another function. Each entry in the array might look like this

const a = next => {
    return action => {
        console.log('dispatching:', action)
        next(action)
    }
}
Copy the code

The last few lines of code

dispatch = compose(... chain)(store.dispatch)return {
  ...store,
  dispatch
}
Copy the code

The code for compose is as follows

export default functioncompose(... funcs) {if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  returnfuncs.reduce((a, b) => (... args) => a(b(... args))) }Copy the code

Compose is a merge method that returns arg => arg when funcs is not passed, returns funcs[0] when funcs is 1, and makes a merge when funcs is longer than 1, for example

const func1 = (a) => {
  return a + 3
}

const func2 = (a) => {
  return a + 2
}

const func3 = (a) => {
  returna + 1 } const chain = [func1, func2, func3] const func4 = compose(... chain)Copy the code

Func4 is such a function

func4 = (args) => func1(func2(func3(args)))
Copy the code

So the above dispatch = compose(… Chain (store.dispatch) is one such function

const chain = [logger, date] dispatch = compose(... Chain)(store.dispatch) // Equivalent to dispatch => Logger (date(store.dispatch))Copy the code

Finally, we pass out the Store object, overwriting the Dispatches in the store with our dispatches

return {
    ...store,
    dispatch
}
Copy the code

Now that the entire applyMiddleware source code analysis is complete, it turns out that it’s not as mysterious as you might expect. Always keep an eye out for knowledge

And hand-written applyMiddleware

There are three differences between applyMiddleware and my hand-written version of it:

  • Native only provides getState and Dispatch, while I hand-write all the properties and methods in the Store
  • Native Middleware can only be used once because it works on createStore; My own handwriting is on store, which can be called multiple times
  • Native calls to store.dispatch can be made from middleware without any side effects, while hand-written ones override the store.dispatch method, which is useful for asynchronous middle

The last

To view the demo

View the source code, welcome star

Your rewards are my motivation to write