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