THE LAST TIME
The last time, I have learned
[THE LAST TIME] has always been a series THAT I wanted to write, aiming to build on and revisit THE front end.
But also to their own shortcomings and technology sharing.
Please refer to the collection of the author’s articles for details:
- GitHub address: Nealyang/personalBlog
- Public number: full stack front end selection
TLT past
- Thoroughly understand JavaScript execution mechanics
- This: call, apply, bind
- A thorough understanding of all JS prototype related knowledge points
- Simple JavaScript modularity
- How TypeScript can be advanced
preface
The concept of paradigm is the core of Kuhn’s paradigm theory, and paradigm is a theoretical system in essence. Kuhn points out that a paradigm is an accepted model or pattern in established usage.
Learning from Redux is not about how complex its source code is, but its idea of state management, which is really worth learning from.
Honestly, the title is really hard to pick, because this is my next redux article. Two pieces put together make a complete Redux.
From Redux design concept to source code analysis
This article continues with the design and source code implementation of combineReducers, applyMiddleware, and compose
As for handwriting, it is also very simple, to put it bluntly, remove the rigorous verification of the source code, is the handwriting on the market. Of course, in this article, I will try to develop the rest of the APIS in the form of a handwritten evolution.
combineReducers
As we learned from the previous article, newState is retrieved in the dispatch function through currentReducer(currentState, Action). Therefore, the final organization of state completely depends on the reducer we passed in. As the app grew and state became more complex, Redux came up with the idea of divide-and-conquer. Although it is ultimately a single root, each branch is processed in a different file or func before the merge is organized. (Modular? Yes?)
CombineReducers are not at the heart of Redux, or rather it is a helper function. But I personally like this feature. Its function is to combine an object with multiple reducer functions as values into a final Reducer function.
The course of evolution
For example, we now need to manage a “huge” state:
let state={
name:'Nealyang'.baseInfo: {age:'25'.gender:'man'
},
other: {github:'https://github.com/Nealyang'.WeChatOfficialAccount:'Full stack Front End Selection'}}Copy the code
Because it is too large, it is difficult to maintain it in a Reducer. So I split into three reducer.
function nameReducer(state, action) {
switch (action.type) {
case "UPDATE":
return action.name;
default:
returnstate; }}function baseInfoReducer(state, action) {
switch (action.type) {
case "UPDATE_AGE":
return {
...state,
age: action.age,
};
case "UPDATE_GENDER":
return {
...state,
age: action.gender,
};
default:
returnstate; }}function otherReducer(state,action){... }Copy the code
In order to form a reducer as we saw above, we need to make this function
const reducer = combineReducers({
name:nameReducer,
baseInfo:baseInfoReducer,
other:otherReducer
})
Copy the code
So, we now write a combineReducers ourselves
function combineReducers(reducers){
const reducerKeys = Object.keys(reducers);
return function (state={},action){
const nextState = {};
for(let i = 0,keyLen = reducerKeys.length; i<keyLen; i++){// Take out the reducers key, i.e. Name, baseInfo, and other
const key = reducerKeys[i];
// Take out the corresponding reducer as shown above: nameReducer, baseInfoReducer, and otherReducer
const reducer = reducers[key];
// Remove the initial state to be passed to the reducer
const preStateKey = state[key];
// Get the state after reducer processing
const nextStateKey = reducer(preStateKey,action);
// assign the value below the corresponding key of the new state
nextState[key] = nextStateKey;
}
returnnextState; }}Copy the code
That’s pretty much it. We’re done.
Reducer for more combinations, splits and uses, please refer to my github open source front-end and back-end Blog Demo: React-Express-blog -Demo
The source code
export type Reducer<S = any, A extends Action = AnyAction> = (
state: S | undefined.action: A
) => S
export type ReducersMapObject<S = any, A extends Action = Action> = {
[K in keyof S]: Reducer<S[K], A>
}
Copy the code
Defines a parameter type to be passed to the combineReducers function. That’s what we have up there
{
name:nameReducer,
baseInfo:baseInfoReducer,
other:otherReducer
}
Copy the code
In fact, a state key was changed, and the value corresponding to the Reducer was the state value of the Reducer that was extracted from the previous key.
export default function combineReducers(reducers: ReducersMapObject) {
// Obtain all keys, which are future state keys and the keys corresponding to the reducer at this time
const reducerKeys = Object.keys(reducers)
// Filter the reducers corresponding reducer to ensure the KV format. Is there anything wrong
const finalReducers: ReducersMapObject = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if(process.env.NODE_ENV ! = ='production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)}}if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
// Get the exact keyArray again
const finalReducerKeys = Object.keys(finalReducers)
// This is used to make sure we don't warn about the same
// keys multiple times.
let unexpectedKeyCache: { [key: string]: true }
if(process.env.NODE_ENV ! = ='production') {
unexpectedKeyCache = {}
}
let shapeAssertionError: Error
try {
// Check the reducer
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
// The key is this function
return function combination(state: StateFromReducersMapObject
= {}, action: AnyAction
) {
if (shapeAssertionError) {
throw shapeAssertionError
}
if(process.env.NODE_ENV ! = ='production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
let hasChanged = false
const nextState: StateFromReducersMapObject<typeof reducers> = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
NextStateForKey is a newState that should not be undefined
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
// Decide whether to change, here actually I am still confused
// Theoretically, reducer newState is not equal to preState no matter whathasChanged = hasChanged || nextStateForKey ! == previousStateForKey } hasChanged = hasChanged || finalReducerKeys.length ! = =Object.keys(state).length
return hasChanged ? nextState : state
}
}
Copy the code
CombineReducers code is actually very simple, the core code is the above abbreviation as we do. But I do like this feature.
applyMiddleware
ApplyMiddleware means Middleware in Redux. The concept of middleware is not unique to Redux. Express, Koa and other frameworks have this concept as well. They’re just there to solve different problems.
Redux’s Middleware is basically an extension, or rewrite, of Dispatch to enhance it! Generally we commonly used can log, error collection, asynchronous call and so on.
In fact, I think the Chinese documentation is pretty good for Redux Middleware, so HERE’s a quick introduction. Those who are interested can check out the detailed introduction: Redux Chinese documentation
Evolution of Middleware
The logging function is enhanced
- Requirements: in each modification
state
Write down the changes beforestate
, why modified, and modifiedstate
. - Action: This is true for each change
dispatch
Initiated, so I’m just gonna be heredispatch
Add a layer of processing and it’s done for good.
const store = createStore(reducer);
const next = store.dispatch;
/* Rewrites store.dispatch*/
store.dispatch = (action) = > {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
Copy the code
As mentioned above, we can log every time we modify the Dispatch. Because we rewrote Dispatch not.
Added an error monitoring enhancement
const store = createStore(reducer);
const next = store.dispatch;
store.dispatch = (action) = > {
try {
next(action);
} catch (err) {
console.error('Error Report:', err)
}
}
Copy the code
So above, we also fulfilled this requirement.
But looking back, how can these two requirements be implemented at the same time and be well decoupled?
Think about it, since we are enhanced Dispatch. So can we pass dispatch as a parameter to our enhancer function?
Multifile enhancement
const exceptionMiddleware = (next) = > (action) => {
try {
/*loggerMiddleware(action); * /
next(action);
} catch (err) {
console.error('Error Report:', err)
}
}
/*loggerMiddleware becomes a parameter */
store.dispatch = exceptionMiddleware(loggerMiddleware);
Copy the code
// Next is the purest store.dispatch
const loggerMiddleware = (next) = > (action) => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
Copy the code
So when you finally use it, it looks like this
const store = createStore(reducer);
const next = store.dispatch;
const loggerMiddleware = (next) = > (action) => {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
const exceptionMiddleware = (next) = > (action) => {
try {
next(action);
} catch (err) {
console.error('Error Report:', err)
}
}
store.dispatch = exceptionMiddleware(loggerMiddleware(next));
Copy the code
As shown above, we can’t separate Middleware to a file because we rely on an external store. So let’s pass store in again!
const store = createStore(reducer);
const next = store.dispatch;
const loggerMiddleware = (store) = > (next) => (action) = > {
console.log('this state', store.getState());
console.log('action', action);
next(action);
console.log('next state', store.getState());
}
const exceptionMiddleware = (store) = > (next) => (action) = > {
try {
next(action);
} catch (err) {
console.error('Error Report:', err)
}
}
const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
store.dispatch = exception(logger(next));
Copy the code
So that’s our Middleware, and in theory, that’s enough. But! Isn’t it a little ugly? And very unintuitive to read?
If I need to add another middleware, the call becomes
store.dispatch = exception(time(logger(action(xxxMid(next)))))
Copy the code
That’s where applyMiddleware comes in.
We just need to know how many middleware there are and call them internally sequentially no
const newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore);
const store = newCreateStore(reducer)
Copy the code
Handwritten applyMiddleware
const applyMiddleware = function (. middlewares) {
// Override the createStore method, which returns a store with an enhanced version of Middleware
return function rewriteCreateStoreFunc(oldCreateStore) {
// Returns a createStore for external calls
return function newCreateStore(reducer, initState) {
// Take out the original store first
const store = oldCreateStore(reducer, initState);
// const chain = [exception, time, logger] Note that this is passed to the Middleware store for the first call
const chain = middlewares.map(middleware= > middleware(store));
// Retrieve the original dispatch
let dispatch = store.dispatch;
// Middleware calls ←, but arrays are →. So the reverse. A second call is then made after passing in Dispatch. And finally, dispatch func.
chain.reverse().map(middleware= > {
dispatch = middleware(dispatch);
});
store.dispatch = dispatch;
returnstore; }}}Copy the code
The explanation is all in the code
In fact, the source code is also such a logic, but the source code implementation is more elegant. He utilizes the compose method of functional programming. Before we look at the source code for applyMiddleware, let’s take a look at the compose method.
compose
For compose, var a = compose(fn1,fn2,fn3,fn4)(x))))) = compose(fn1,fn2,fn3,fn4).
For compose, the result is a function. The parameters passed by the call to the compose function will be used as arguments to the last parameter of the compose function, which will be called from the inside out like an onion ring.
export default function compose(. funcs: Function[]) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return <T>(arg: T) => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (... args: any) => a(b(... args))) }Copy the code
Oh and clear! A bit of a puzzle is there ~ functional programming is brain-burning 🤯 and direct. So people who love love very much.
Compose is a common way of composing functions in functional programming.
The method is simple: the parameter passed in is func[], and if there is only one, the result of the call is returned. If multiple, then funcs.reduce((a, b) => (… args: any) => a(b(… args))).
Let’s go straight to the last line
import {componse} from 'redux'
function add1(str) {
return 1 + str;
}
function add2(str) {
return 2 + str;
}
function add3(a, b) {
return a + b;
}
let str = compose(add1,add2,add3)('x'.'y')
console.log(str)
// output result '12xy'
Copy the code
dispatch = compose
(… Chain)(Store.dispatch) applyMiddleware’s last line of source code is this. In fact, even though we wrote the reverse part up there.
Reduce is an es5 array method that applies a function to the accumulator and each element in the array (from left to right) to reduce it to a single value. The signature of the function is arr.reduce(callback[, initialValue])
So if we look at it this way:
[func1,func2,func3].reduce(function(a,b){
return function(. args){
returna(b(... args)) } })Copy the code
Every time a reduce is performed, a callback is a(b(… Args)) function, of course, the first time is a is func1. And then there’s an infinite stack of people. The result was a func1(func2(func3(… The function of the args))).
conclusion
So looking back, that’s all redux really is, and the first article is kind of the core of Redux, the idea and the way of state management. The second chapter can be understood as redux’s own small ecology. The entire code is no more than two or three hundred lines. But this paradigm of state management is very much for us to think about, learn from and learn from.
Study and communication
- Pay attention to the public number [full stack front selection], get good articles recommended every day
- Add wechat id: is_Nealyang (note source) to join group communication
Public account [Full stack front End Selection] | Personal wechat 【is_Nealyang】 |
---|---|