preface

Redux is also one of THE articles I listed in THE LAST TIME series, as I’m exploring a solution for state management in THE business I’m currently developing. So, the idea here is to learn from Redux, from his state. After all, success always requires standing on the shoulders of giants.

Then again, writing about Redux in 2020 is a bit dated. However, Redux must have looked back at Flux, CQRS, ES, etc.


This article first from the Redux design concept to part of the source code analysis. Next we’ll focus on how Redux Middleware works. For handwriting, I recommend: Fully understand Redux (achieving a REdux from zero)

Redux

Redux is not particularly Giao tech, but the concept is really good.

Basically, it’s a big closure that provides a setter and getter. Plus a pubSub… What else reducer, middleware, or actions are based on his rules and addressing user pain points, nothing more. Let’s talk a little bit…

Design idea

Back in the jQuery era, we were process-oriented, but with react’s popularity, we came up with state-driven UI development. We think of a Web application as a one-to-one correspondence between state and UI.

But as our Web applications become more and more complex, the state behind an application becomes more and more difficult to manage.

Redux is a state management solution for our Web application.

One to one correspondence

As shown in the figure above, a store is a state container provided by Redux. It stores all the states needed by the View layer. Each UI corresponds to a state behind it. Redux does the same. One state corresponds to one View. As long as the state is the same, the View is the same. State drives the UI.

Why use itRedux

As mentioned above, we now have a state-driven UI, so why do we need Redux to manage state? React itself is a state drive view but not.

The reason is that the front end is becoming more and more complex. Often a front-end application has a lot of complex, irregular interactions. It is accompanied by various asynchronous operations.

Any operation could change the state, which would result in our application of state becoming more and more chaotic, and the passive cause becoming more and more obscure. It’s easy to lose control of when, why, and how these states occur.


As mentioned above, if our page is complex enough, the state changes behind the view might look something like this. There is parent-child communication, sibling communication, parent-child communication, and even cross-hierarchy communication between components.

Our ideal state management would look something like this:


Purely on an architectural level, the UI is completely separate from the state, and one-way data flow ensures that the state is manageable.


And that’s what Redux does!

  • eachStateIs predictable
  • Unified management of actions and states

Let’s take a look at some of the concepts in Redux. In fact, beginners are often confused by the concept.

store

Where the data is stored, you can think of it as a container, there’s only one Store for the entire app.

State

The value of the application state stored at a certain time

Action

View is a notification to change state

Action Creator

You can think of it as the Action factory function

dispatch

View emits the Action medium. It’s the only way

reducer

Combine a new State based on the actions and states currently received. Notice that it has to be pure

The three principles

The use of Redux is based on three principles

Single data source

Single data source This is perhaps the biggest difference from Flux. In Redux, the state of the entire application is stored in an object. Of course, this is the only place to store app state. An Object tree is an Object tree. Different branches correspond to different components. But ultimately there is only one root.

Also benefits from a single State tree. “Undo/redo” or even playback that was previously difficult to achieve. It’s a lot easier.

The State read-only

The only way to change state is to dispatch an action. Action is just a token. Normal Object.

Any state changes can be understood as non-view layer changes (network requests, user clicks, etc.). The View layer simply emits an intent. How to satisfy is entirely up to Redux itself, namely reducer.

store.dispatch({
  type:'FETCH_START',
  params:{
    itemId:233333
  }
})
Copy the code

Use pure functions to modify

The so-called pure function, you have to be pure, don’t change around. I won’t go into the written words here. The pure function modification we said here is actually the reducer we said above.

A Reducer is a pure function that accepts the current state and action. Then return a new state. So here, the state is not updated, it’s just replaced.

The reason for pure functions is predictability. As long as the incoming state and action are always the same, it can be understood that the new state returned is always the same.

conclusion

There’s more to Redux than that. There are other things like middleware, actionCreator, etc. In fact, they are all derivatives in the process of use. We mainly understand the idea. Then go to the source code to learn how to use.


Source code analysis

The Redux source code itself is pretty simple, so we’ll cover compose, combineReducers, and applyMiddleware in the next article

The directory structure

Redux source code itself is very simple, code is not much. Learning it is mainly to learn his programming ideas and design paradigms.

Of course, we can also take a look at the Redux code to see how the big guys use TS. So in the source code analysis, we also spend a lot of effort to look at the Redux type description. So let’s start with type

src/types

The purpose of looking at type declarations is to learn about Redux’s TS type declarations. So we’re not going to repeat the form of similar statements.

actions.ts

Type declarations don’t have much logic to say either, so I’ll just comment them out

// Interface definition for Action. The type field is explicitly declared

export interface Action<T = any> {

  type: T

}

export interface AnyAction extends Action {

  // Add additional arbitrary fields to the Action interface (we usually write the AnyAction type, with a "base class" to constrain the type field)

  [extraProps: string]: any

}

export interface ActionCreator<A> {

  // Function interface, generic constraint function return A

(... args: any[]): A

}

export interface ActionCreatorsMapObject<A = any> {

  // Object with the value ActionCreator

  [key: string]: ActionCreator<A>

}

Copy the code

reducers.ts

// define A function that takes S and inherits Action's A by default AnyAction and returns S

export type Reducer<S = any, A extends Action = AnyAction> = (

  state: S | undefined.

  action: A

) => S



// It can be understood that the key of S serves as the key of the ReducersMapObject, and then the value is the Reducer function. In we can think of as traversal

export type ReducersMapObject<S = any, A extends Action = Action> = {

  [K in keyof S]: Reducer<S[K], A>

}

Copy the code

The above two statements are relatively straightforward. The next two are a little more difficult

export type StateFromReducersMapObject<M> = M extends ReducersMapObject<

  any,

  any

>

  ? { [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never }

  : never

  

export type ReducerFromReducersMapObject<M> = M extends {

  [P in keyof M]: infer R

}

  ? R extends Reducer<any, any>

    ? R

    : never

  : never

Copy the code

Let’s explain the first of the two statements (a little more difficult).

  • StateFromReducersMapObjectAdd another genericMThe constraint
  • MIf inheritanceReducersMapObject<any,any>Then go{ [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never }The logic of the
  • Otherwise it isnever. What is not
  • { [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never }Obviously, this is an object,keyfromMInside the object, which isReducersMapObjectIncoming from insideS.keyThe correspondingvalueIt’s a judgment callM[P]Whether inherited fromReducer. Otherwise it’s nothing
  • inferKey words andextendsAlways cooperate with use. In this case, it means returnReducertheState The type of

other

Other types in the types directory, such as Store and Middleware, are declared in this way, but can be read if you are interested. Then take its essence and apply it to their TS projects

src/createStore.ts

Don’t be confused by the way function overloading is written above

As you can see, the entire createstore. ts is a createStore function.


createStore

Three parameters:

  • reducer: is reducer, pure Function of newState is calculated according to action and currentState
  • preloadedState: the initial State
  • enhancer: Enhance the store to have third-party functionality

CreateStore is a collection of closure functions.


INIT

// A extends Action

dispatch({ type: ActionTypes.INIT } as A)

Copy the code

This method is reserved for Redux to initialize State, which means dispatch goes to the branch of our default Switch Case Default and gets the default State.

return

const store = ({

    dispatch: dispatch as Dispatch<A>,

    subscribe,

    getState,

    replaceReducer,

    [?observable]: observable

  } as unknown) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext

Copy the code

Ts returns dispatch, subscribe, getState, replaceReducer, and [? Observable].

Here we briefly introduce the implementation of the first three methods.

getState

  function getState() :S {

    if (isDispatching) {

      throw new Error(

        I reducer is executing, newState is producing! Not now.

      )

    }



    return currentState as S

  }

Copy the code

And the simple way to do that is return currentState

subscribe

Subscribe adds a listener that is called on each Dispatch action.

Returns a function that removes this listener.

Use as follows:

const unsubscribe = store.subscribe((a)= >

  console.log(store.getState())

)



unsubscribe();

Copy the code
function subscribe(listener: () = >void{

    // If listenter is not a function, I will report an error (ts static check can check this, but! That's compile time, this is run time.)

    if (typeoflistener ! = ='function') {

      throw new Error('Expected the listener to be a function.')

    }

    // The same sample as getState

    if (isDispatching) {

      throw new Error(

        'You may not call store.subscribe() while the reducer is executing. ' +

        'If you would like to be notified after the store has been updated, subscribe from a ' +

        'component and invoke store.getState() in the callback to access the latest state. ' +

        'See https://`Redux`.js.org/api/store#subscribelistener for more details.'

      )

    }



    let isSubscribed = true



    ensureCanMutateNextListeners()

    // Add the listeners directly to the nextListeners

    nextListeners.push(listener)



    return function unsubscribe({// Also use closures to see if the subscription is available, and then remove the subscription

      if(! isSubscribed) {

        return

      }



      if (isDispatching) {

        throw new Error(

          'You may not unsubscribe from a store listener while the reducer is executing. ' +

          'See https://`Redux`.js.org/api/store#subscribelistener for more details.'

        )

      }



      isSubscribed = false// Modify the subscription status



      ensureCanMutateNextListeners()

      // Find the location and remove the listener

      const index = nextListeners.indexOf(listener) 

      nextListeners.splice(index, 1)

      currentListeners = null

    }

  }

Copy the code

An explanation is to add a function to the listeners

Say here ensureCanMutateNextListeners again, how many story source that have mentioned this method. It’s also a little confusing to me.

The implementation of this method is very simple. Check whether the current listening array is equal to the next array. If it is! Then I have a copy.

  let currentListeners: (() = > void|) []null = []

  let nextListeners = currentListeners

  

  function ensureCanMutateNextListeners({

    if (nextListeners === currentListeners) {

      nextListeners = currentListeners.slice()

    }

  }

Copy the code

So why? Leave an egg here. Wait until after Dispatch to check out this puzzle.

dispatch

  function dispatch(action: A{

  // Action must be a normal object

    if(! isPlainObject(action)) {

      throw new Error(

        'Actions must be plain objects. ' +

        'Use custom middleware for async actions.'

      )

    }

  // Must contain the type field

    if (typeof action.type === 'undefined') {

      throw new Error(

        'Actions may not have an undefined "type" property. ' +

        'Have you misspelled a constant? '

      )

    }

  / / same as above

    if (isDispatching) {

      throw new Error('Reducers may not dispatch actions.')

    }



    try {

      // Set the dispatch tag to true (explains where those judgments are coming from)

      isDispatching = true

      // New state from the reducer passed in

      // let currentReducer = reducer

      currentState = currentReducer(currentState, action)

    } finally {

    // Change the status

      isDispatching = false

    }

    

    / / will nextListener assigned to currentListeners, listeners (note that review ensureCanMutateNextListeners

    const listeners = (currentListeners = nextListeners)

    // Trigger the listener one by one

    for (let i = 0; i < listeners.length; i++) {

      const listener = listeners[i]

      listener()

    }



    return action

  }

Copy the code

The method is very simple. It’s all in the comments. Here we are again looking back on it ensureCanMutateNextListeners meaning

ensureCanMutateNextListeners

  let currentListeners: (() = > void|) []null = []

  let nextListeners = currentListeners

  

  function ensureCanMutateNextListeners({

    if (nextListeners === currentListeners) {

      nextListeners = currentListeners.slice()

    }

  }



  function subscribe(listener: () = >void{

    // ...

    ensureCanMutateNextListeners()

    nextListeners.push(listener)



    return function unsubscribe({

      ensureCanMutateNextListeners()

      const index = nextListeners.indexOf(listener)

      nextListeners.splice(index, 1)

      currentListeners = null

    }

  }

  

  function dispatch(action: A{

    / /...

    const listeners = (currentListeners = nextListeners)

    for (let i = 0; i < listeners.length; i++) {

      const listener = listeners[i]

      listener()

    }

    // ...

    return action

  }

Copy the code

From above, it looks as if all the code needs is an array to store the listener. But the fact is, we are exactly our listeners that can be unSubscribe. Slice also changes the array size.

We add a copy of listeners to avoid missing listeners because of the subscribe or unsubscribe changes made to the listeners.

The last

Limited space, write this for the time being ~

Middleware, which I’m going to focus on later, is a more generic form of Middleware, and it’s not even Redux. By this point, you can write your own state management plan.

And combineReducers is also I think is clever design fee. So these pages, I will move to the next ~


Refer to the link

  • redux
  • See all 10 lines of codeReduximplementation
  • ReduxChinese document

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】