React-redux has always been used in projects to use redux, but THE principle of redux has never been thoroughly understood!! So recently class is little ~ can study carefully!! 🤔 🤔

As I read, I ask myself a lot of questions, and then I go around looking for answers, so I get something out of school!

This article is for those of you who have used Redux

As for the source version, 4.0 is still used, and TS version is not mentioned. I think if the TS version is used for analysis, it will involve explaining a lot of type judgment, which is not conducive to the readability of the article

The article is a bit long, and the general contents are as follows: Util folder –actionTypes –isPlainObject Why redux does this? / / warnings SRC folder / / index / / createStore / / Currify / / higher order functions / / help understand applyMiddleware — Compose (function combination, reduce, reduceRight method) –appMiddlewares “-bindActionCreators” – “Combiner Creators call Replacer Creators” The reducer must be a pure reducer function. The reducer must be a reducer function

Utils folder

We usually start with the index file when we read the source code, but I thought it would be useful to look at the utility class functions first, so we’ll start with the utils file:

actionTypes

// Generate a random string const randomString = () => math.random ().toString(36).substring(7).split('').join('.') const ActionTypes = { INIT: `@@redux/INIT${randomString()}`, REPLACE: `@@redux/REPLACE${randomString()}`, PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}` } export default ActionTypesCopy the code

This file is used to define three reserved types of redux:

INIT(Reducer initialization type)

REPLACE(Reducer replacement type)

PROBE_UNKNOWN_ACTION(Random type)

At the same time, to ensure the uniqueness of the three types, a randomly generated string is added to the end of each type

*isPlainObject

Export default function isPlainObject(obj) {// Obj must be a non-empty object if (typeof obj! = = 'object' | | obj = = = null) return false let proto = obj along the prototype chain / / lookup, up until the proto (father) is null when the stop at the next higher level, While (object.getProtoTypeof (proto)! == null) {proto = object.getProtoTypeof (proto)} Null => an object => plainObject return Object.getProtoTypeof (obj) === proto}Copy the code

This function may seem mundane on the surface, mainly to determine if an object is a simple object, but it has some interesting features

A plain object is an object without an official definition.

In the above function, the argument obj can only be an Object created from a literal ({}), or from a new Object(),

Obj cannot be an Object created with Object.create(), even if object.create (null),

Of course, when looking at the source code, why this judgment can not be directly written, clearly can achieve the same function 🧐🧐

let proto = Object.getPrototypeOf(obj) return !! proto && Object.getPrototypeOf(proto) === nullCopy the code

Later I saw in a Commit history on Redux’s Github,He used to write it like this:

There is always some justification for change, as discussed in redux’s Github issue,This is what the author says:The translation reads as follows:

It handles cross-domain objects by iterating through the prototype chain until it reaches the Object, and checking if the “Object” has the same prototype as the normal Object we need to determine.

For objects from iframe, this object is different from our native objects because they come from two different JavaScript contexts. Therefore, we use this technique to get the Object definition of an Object instance.

It’s a good performance boost because you can short-circuit them in a fair number of cases before going into the slower toString() calls (which are the parts that slow lodash/isPlainObject and IS-Plain-Object).

In the third sentence, I understand that it can improve performance by terminating some work before calling toString() (if you have a better understanding, let me know in the comments section 😝😝😝😝).

In general, the benefits of using circular judgment are as follows:

1. Resolve cross-realm object issues

2. Can have higher performance

warning

export default function warning(message) { if (typeof console ! == 'undefined' && typeof console.error === 'function') { console.error(message) } try { throw new Error(message) } catch  (e) {} }Copy the code

This function encapsulates the error message

index

function isCrushed() {} if ( process.env.NODE_ENV ! == 'production' && typeof isCrushed.name === 'string' && isCrushed.name ! == 'isCrushed' ) { ... Warning Message} export {createStore, combineReducers, bindActionCreators, applyMiddleware, compose, __DO_NOT_USE__ActionTypes}Copy the code

In addition to exposing methods, the index file serves as an entry point to the code:

I don’t know if you noticed the following function:

function isCrushed() {}

This is a pseudo function

In the case of compressed functions, the function name is replaced with a single letter or even an _

Therefore, we can use this pseudo-function to determine whether code compression exists in the development environment

If such a case exists, an error is thrown

if ( process.env.NODE_ENV ! == 'production' && typeof isCrushed.name === 'string' && isCrushed.name ! == 'isCrushed' ) { warning( 'You are currently using minified code outside of NODE_ENV === "production". ' + 'This means  that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' + 'to ensure you have the correct code for your production build.' ) }Copy the code

Before we look at the source code, let’s think about how a store would be created in a real Redux project:

In this case, enhancer is the equivalent of a collection of middleware components composed by compose via the applyMiddleware application (which may sound like a mouthful 😸).

createStore

CreateStore is redux’s core block, and its job is to create a Store object, And exposed ensureCanMutateNextListeners, getState, subscribe, dispatch, replaceReducer, observables that several methods

export default function createStore(reducer, preloadedState, Enhancer) {/ / some judgment function ensureCanMutateNextListeners () {} function getState () {} function the subscribe () {} function dispatch() {} function replaceReducer() {} function observable() {} dispatch({ type: ActionTypes.INIT }) }Copy the code

[$$observable]

The first line of the createStore code is:

import $$observable from 'symbol-observable'

What is his role?

In thisThe readme of NPM package”Is described as followsIt means to matchRedux,RxJSIn such libraries, our objects become Observables, and we need to avoid calling error, complete, and unsubscribe after calling error, complete, and unsubscribe

In actual development, we usually don’t use this method 👻👻👻👻

export default function createStore(reducer, preloadedState, enhancer) {

}
Copy the code

CreateStore receives three parameters — reducer (function), preloadedState (initial state), enhancer (function, a higher-order function). These three parameters are clearly explained in the official document:

Some of the judgment

if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function.'
    )
  }
Copy the code

This determination is intended to guarantee the single enhancer principle. If there are multiple enhancers, they need to be grouped together by compose

if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
Copy the code

The preloadedState parameter is optional, so when the second parameter of createStore is a function and the third parameter is undefined, the second parameter is passed in enhancer by default. PreloadedState and enhancer cannot be functions at the same time. Therefore, preloadedState cannot be functions at the same time. PreloadedState is of type any. That was a bit of a misnomer, but I found the answer in Github issues:

It’s not really that important, but it would be nice to have an official explanation 👻🤡😸🤠 college 👻 👻

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

Enhance the function of the store, let it have the function of the third party, such as middleware. ApplyMiddleware () is an enhancer (talking about applyMiddleware specifically 🧐)

This is also equivalent to leaving the store creation process entirely to the middleware in the presence of enhancer

Build store initialization

CurrentState let currentState = preloadedState // store the current event listener let CurrentListeners = [] // The current event listeners are referenced. Let nextListeners = currentListeners // Whether Actions (false) are currently being sent falseCopy the code

ensureCanMutateNextListeners

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }
Copy the code

This is the internal method of createStore, and in code, it just feels normal to copy currentListeners and assign values to nextListeners when nextListeners are equal to currentListeners. (More on why 👻 later)

getState

function getState() { //... If an action is being issued, the call will throw an error return currentState}Copy the code

Return the current state directly

subscribe

Function subscribe(listener) {// If (typeof listener! == 'function') { ... } // An error is thrown when the actions are being dispatched. } / / subscription state is set to true the let isSubscribed = true / / copy of the current currentListeners ensureCanMutateNextListeners () // Listeners are added to the listeners. Push (listener) return function unsubscribe() { }}Copy the code

unsubscribe

function subscribe(listener) { ... Return function unsubscribe() {// Unsubscribe if (! IsDispatching) {return} // An error is raised while the actions are being dispatched. } // Unsubscribe isSubscribed = false // Copy again, Cancel the current listeners ensureCanMutateNextListeners () const index = nextListeners. IndexOf (the listener) nextListeners. Splice (index, 1) currentListeners = null } }Copy the code

Here, let’s take a look backensureCanMutateNextListeners, why copy and not use directlycurrentListers

The idea is to avoid calling subscribe/unsubscribe during dispatch

dispatch

Function dispatch(action) {// Action must be pure if (! isPlainObject(action)) { ... } // If (typeof action.type === 'undefined') {... } // If actions are being sent, an error is reported. Avoid calling dispatch from the reducer, and dispatch calls the reducer again, forming a loop... if (isDispatching) { ... } try {// Set the current state to isDispatching = true // Pass in the reducer the current state and the state to change, CurrentState = currentReducer(currentState, action)} finally { // Assign values to currentListeners and Listeners respectively, and trigger listeners one by one; const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } return action }Copy the code

replaceReducer

Function replaceReducer(nextReducer) {// nextReducer must be a function if (typeof nextReducer! == 'function') { ... } // change currentReducer currentReducer = nextReducer // notify all reducer state changes dispatch({type: actiontypes.replace})}Copy the code

For the use scenario of replaceReducer, I only saw code division in the official website, and hot update mechanism and dynamic load different reducer in other places

observable

RxJS-based middleware for Redux that allows developers to work with async actions
Copy the code

He is a middleware based on RxJS, used for asynchronous action, in our daily use, actually not how involved, just ignore it 🤔

Complete the initialization of the Store

dispatch({ type: ActionTypes.INIT })
Copy the code

Expose some methods

return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
Copy the code

Currization, higher order functions

Before we get into compose and Applymiddleware, we need to do a quick primer on what compose and applymiddleware are about, so if you already know something, skip it!!

Conceptually:

  • Higher-order functions: are receiving functions asparameter, or a function as a returnThe outputthefunction
React-redux connect()();Copy the code
  • Corrification: acceptanceMultiple parametersIs converted to accept aSingle parameter(the first argument to the original function), and returnsAccept the remaining parametersAnd return the resultThe new function(I think of currization as an application of higher-order functions)
Function add(x, y){return x + y; Function (x) {return function(y) {return x + y}} curryAdd(1)(2) // 3Copy the code

None of the above are covered in detail, they cover a lot of functional programming patterns, and if they were covered in detail, it would be a long story, but in terms of learning redux, let’s just get a rough idea of what they mean

compose

Compose refers to a combination of functions, which is an important idea in functional programming, but not limited to it

The function of REDUx is to realize the combination of any, various and different function modules, so as to enhance the function of REDUx

export default function compose(... Funcs) {if (funcs.length === 0) {return arg => arg} If (funcs.length === 1) {return funcs[0]} 😝 return funcs. Reduce ((a, b) => (... args) => a(b(... args))) }Copy the code

Barring some judgment, the core code for Compose is that last sentence

  • First of all, yesArray.prototype.reduceMethod, reduce method is actually very powerful, more on thatThe document
His syntax arr. Reduce (callback,[initialValue]) his function from left to right, Callback: The function contains four arguments - previousValue (the value returned from the last callback call, Or the provided initialValue (initialValue) -currentvalue (the element in the array that is currently being processed) -index (the index of the current element in the array) -array (the array that was called) initialValue (as the first call Const arr = [1, 2, 3] const sum = arr. Reduce ((sum, val) => {return sum + val; }, 0); //sum: 6Copy the code

The law of the composer

compose(a, b) => (... args) => a(b(... args)) compose(a, b, c) => (... args) => a(b(c(... args)))Copy the code

That is, it does it in the reverse order of the input order, the result of the previous function as the input of the next function

This is very similar to koA’s onion ring model(aop)

Here’s another picture to illustrate:In fact, we can also find that this is not reduceRight method! = >The documentIn other words, the compose function can be written like this:

export default function compose(... funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } const last = funcs[funcs.length - 1]; const rest = funcs.slice(0, -1); return (... args) => rest.reduceRight((composed, f) => f(composed), last(... args)) }Copy the code

And according to the submitted records, the reduceRight method was really used before

As for the reason for the change, the author replied as follows:

That means it’s one to three times faster in terms of performance

Finally, note that compose returns a function, which is composed (a,b)() only when it is called.

applyMiddleware

By default, the Redux store created by createStore() does not use Middleware, so it only supports synchronous data streams.

We can use applyMiddleware() to enhance createStore(). This is not necessary, but it will help you to describe asynchronous actions in an easy way.

Not using Middleware:After using Middleware:

Look at the source code again:

export default function applyMiddleware(... middlewares) { return createStore => (... args) => { const store = createStore(... args) let dispatch = () => { throw new Error( 'Dispatching while constructing 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

We go through it paragraph by paragraph

return createStore => (... args) => { const store = createStore(... args) ... }Copy the code

Before analyzing, a recap:

Use of Applymiddleware from Redux

const store = createStore(todos, ['Use Redux'], applyMiddleware(logger))
Copy the code

The call to enhancer(Applymiddleware) in createStore when enhancer exists and is a function

return enhancer(createStore)(reducer, preloadedState)
Copy the code

In combination, this call is equivalent to

applyMiddleware(logger)(createStore)(todos, ['Use Redux'])
or
applyMiddleware(middlewware)(createStore)(reducer, preloadedState)
Copy the code

What does this format make us think of, Currization, right?! And think about the order in which corrification runs!

With that in mind, let’s take a look at the source code

// createStore is the createStore function... Reducer and preloadedState return createStore => (reducer and preloadedState) Const store = createStore(... args) => {// Create a store using the method passed in. args) }Copy the code
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

The meaning of this code is that the call to Dispatch is prohibited during middleware builds. Why?

The reason for the existence of middleware is to enhance the functions of Redux, that is, to enhance the functions of Dispatch. Before the dispatch is assembled, it does not cover the original Dispatch of Redux, which may lead to some functions that cannot be realized.

Redux, for example, originally only supports synchronous actions, but can only support asynchronous actions if middleware is used. If this middleware is not built, calling Dispatch will not be able to distribute asynchronous actions

At the same time, the purpose of defining dispatches with lets is to make it easy to change references to dispatches

const middlewareAPI = { getState: store.getState, dispatch: (... args) => dispatch(... Middlewares.map (Middleware => middlewareAPI) const chain = middlewares.map(middleware => middlewareAPI) Dispatch = compose(... chain)(store.dispatch)Copy the code

The middlewareAPI is a built-in object for applyMiddleware

The middlewareAPI is passed to each middleware as a parameter when traversing the middleware (middlewares.map), passing the result back to chain

Combine redux-Thunk source code to analyze this is a process

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;
Copy the code

The above code is currified. Let’s convert it to ES5:

Function createThunkMiddleware(extraArgument) {return function({dispatch, Return function(next) {return function(next) {return function(next) {return function(next) {return function(next) {return function(next) {return function(next) {return function(next) { Dispatch return function(action) { If (typeof Action === 'function') {return action(dispatch, getState, extraArgument); } // If the argument is not a function, call the original dispatch return next(action); }; }}}Copy the code

It’s a little bit convoluted here, but let’s rearrange the process a little bit

  • Middleware is in the form of ({getStore, dispatch}) = > (next) = > (action) = > {… }

  • A middlewareAPI object is passed to the first-layer function of the middleware through traversal

  • (next)=>(action)={… } function

  • Compose =>(next)=>(action)={… }

  • Compose is composed in such a way that the next (action)={… } as an argument to the next function, next

  • The next of the last combinatorial function is store.dispatch

  • Finally, the compose function (action)={… } assigns to Dispatch

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

Returns the created store and wrapped dispatch

Here’s another example of what middleware can do: 👻👻👻👻 port

------>

bindActionCreator

Action Creator is a function that creates actions. It is like a factory. It is only responsible for action creation (create), but not dispatch (dispatch).

So what does bindActionCreator do? He basically upgraded the factory so that it could both create and distribute actions

Let’s take a look at the definition on the official website:

Convert an object with a value of a different Action Creator into an object with a key of the same name. Each Action Creator is also wrapped with Dispatch so that they can be invoked directly.

Normally you can call Dispatch directly on a Store instance. If you use Redux in React, react-Redux will provide a dispatch function that you can call directly.

Look at the source code of bindActionCreator

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}
Copy the code

He returns a function that implements the functionality of the Dispatch action

Let’s go back to Binge Creators

Export default function bindActionCreators(actionCreators, Dispatch) {// When actionCreators is a single function, Call bindActionCreator directly if (typeof actionCreators === 'function') {return bindActionCreator(actionCreators, Dispatch)} // An error is reported when actionCreators is not an object. If (Typeof actionCreators! == 'object' || actionCreators === null) { ... Throw an error} // Create a new object const boundActionCreators = {} // Iterate over the actionCreators object so that each of its entries, Call bindActionCreator for (const Key in actionCreators) {const actionCreator = actionCreators[key] if (typeof) actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, Dispatch)}} // Finally returns a new object return boundActionCreators}Copy the code

So for this actionCreators

When other files are introduced, it becomes an object:

For a single actionCreator, after calling bindActionCreator, it becomes

text => dispatch(addTodo('text');
Copy the code

For multiple ActionCreators, after calling bindActionCreators, this will become

{
   addTodo : text => dispatch(addTodo('text'));
   removeTodo : id => dispatch(removeTodo('id'));
}
Copy the code

Thus the dispatch call is implemented

combineReducers

In projects, we often use combineReducers like this to combine multiple different Reducer functions

Let’s look at the source code

Export default Function combineReducers(reducers) {Reducer yes, Key const reducerKeys = object. keys(reducers) // ReducerReducers = {} 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}"`) } } // When Reducer is function, Equivalent to a reducer copy to finalReducers if (Typeof Reducers[key] === 'function') {finalReducers[key] = reducers[key]}} const FinalReducerKeys = Object.keys(finalReducers) // In non-production environment, Let unexpectedKeyCache if (process.env.node_env! == 'production') { unexpectedKeyCache = {} } let shapeAssertionError try { // Check that the reducer in finalReducer received a {actiontypes.init} or actiontypes.probe_unknown_action (), AssertReducerShape (finalReducers)} Catch (e) {shapeAssertionError = e} // This returned function is a combined reduce // Return function combination(state = {}, Action) {// If (shapeAssertionError) {throw shapeAssertionError} if (process.env.NODE_ENV ! == 'production') { For example, check whether // state is a pure object // reducers is empty // inputState key does not filter inputState in Reducers and unexpectedKeyCache //... const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, UnexpectedKeyCache) if (warningMessage) {warning(warningMessage)}} // define a change identifier let hasChanged = false // define new state Const nextState = {} // Go through the reducerKeys for (let I = 0; i < finalReducerKeys.length; I ++) {// Obtain key and Reducer const key = finalReducerKeys[I] const Reducer = finalReducers[key] // Obtain state const reducer corresponding to the current key PreviousStateForKey = state[key] // Change state const nextStateForKey = Reducer (previousStateForKey, If (typeof nextStateForKey === 'undefined') {// Reducer cannot return undefined const errorMessage = getUndefinedStateErrorMessage(key, NextState [key] = nextStateForKey nextState[key] = nextStateForKey So every time the change of the reducer is always to return to a new state object hasChanged = hasChanged | | nextStateForKey! == previousStateForKey} // When using CombineReducers to call replaceReducers and remove one of the reducers passed to it, Not update status hasChanged = hasChanged | | finalReducerKeys. Length! == object.keys (state).length // Return state hasChanged? nextState : state } }Copy the code

Reason for repeating hasChanged: discussion in Github Issue

Just a little bit about what I’ve learned

  • function isCrushed() {}

The name attribute of the pseudo function is used to determine whether code compression exists in the development environment

  • Compose the composition function

Reduce method and reduceRight method

  • The discriminant method of pure function (isPlainObject)

Lodash, jq, has in the story

  • Higher order functions, Currization

In redux source code, a lot of places are used to higher-order functions, especially middleware that piece, at first will only feel like looking at the clouds in the fog, read more feel like this writing is very concise

  • Publish and subscribe

Publish-subscribe functions and observer patterns (they are often lumped together, but there are subtle differences, although they are functionally similar)

  • Redux’s three principles

Single data source: The state of the entire application is stored in an object tree that exists in a single store

State is read-only: the only way to change State is to trigger an action, which is a generic object that describes events that have occurred.

Use pure functions to perform modifications: To describe how an action changes the State Tree, you need to write reducers

  • Pure functions

The same input always produces the same output

The only way a function interacts with the outside world is by passing in parameters and returning values

No side effects

  • Why does reduer have to be pure

Let’s look at this code again, and we’re doing it by judgmenthasChangedWhether it istrueTo determine whether to return the new state or the old statestatethe

HasChanged is again determined by comparing the references to nextStateForKey and previousStateForKey

So look at this code again:nextStateForKeyIs through thepreviousStateForKeyTo obtain the

ifreducerInstead of doing it purely as a function, that’s how we’re going to write itreducer And that would lead tostateUnable to update becausestateThe reference is unchanged

Why do we make judgments based on references instead of attribute values? Deep comparisons are performance-consuming such as deep copy, so Redux simply imposed a pure function constraint on reducer

  • Redux data flow

😝😝😝😝😝😝😝

The user triggered an action through an interaction event, and the action was packaged by middleware and sent to the reducer

Reducer changes state according to the action instructions. The page View has been monitoring the data changes in the store, and the new state will be rendered on the page after the changes, so as to achieve user response

  • Why can’t Redux just send out asynchronous actions? Check it out for yourself.

To summarize: It is possible to implement asynchronous data flows without middleware, but it is not convenient to do so in large projects

Because we might be executing the same action in different components, you might want to debounce some actions. Or some local state (such as auto-increment ID) is more closely related to Action Creator

Therefore, from a maintenance perspective, it would be better to extract Action Creator

Redux Thunk or Redux Promise is just a grammar sugar, which helps us dispatching Thunk or Promise, but we are not necessarily using him

This means that with middleware, we don’t have to think about asynchronous synchronization and can focus on components. Redux-saga was also mentioned in the discussion, which is interesting. Next time, 😝

Think of… The list goes on, like Redux, mobx differences, functional programming blah blah blah

conclusion

The process of learning the source code is still very difficult 😵, need to take a moment to guess why, for appyMiddleware this part, go to the debugger!

There may be some places in the article that need not be rigorous, welcome to discuss with me in the comment area! 😸

The reference link 🔗 is basically in the original article and the redux website link is at the end