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 modificationstateWrite down the changes beforestate, why modified, and modifiedstate.
  • Action: This is true for each changedispatchInitiated, so I’m just gonna be heredispatchAdd 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】