Tool file

actionTypes.js

const randomString = () =>
  Math.random().toString(36).substring(7).split('').join('.')

const ActionTypes = {
  INIT: `@@redux/INIT${/* #__PURE__ */ randomString()}`,
  REPLACE: `@@redux/REPLACE${/* #__PURE__ */ randomString()}`,
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}

export default ActionTypes
Copy the code

The actionTypes file mainly encapsulates the actionType built into Redux, where actiontypes.init is used to initialize the store. REPLACE PROBE_UNKNOWN_ACTION replaces the reducer actionType.

Math.random().tostring (36

Syntax: number.toString(radix) Definition and usage: string representation of numbers For example, when radix is 2, NumberObject is converted to a string represented by binary values. Radix parameter: Specifies the radix that represents the number, which is an integer between 2 and 36. If this parameter is omitted, the cardinality 10 is used. Note, however, that if the parameter is a value other than 10, the ECMAScript standard allows the implementation to return any value. 2-digits displayed in binary values 8-digits displayed in octal values 16-digits displayed in hexadecimal values 0-9 (10 digits) + a-Z (26 English letters) total 36, so Redix <=36.Copy the code

isPlainObject.js

export default function isPlainObject(obj: any): boolean { if (typeof obj ! = = 'object' | | obj = = = null) return false let proto = obj / / get the best prototype, if is their own, so that is pure object. while (Object.getPrototypeOf(proto) ! == null) { proto = Object.getPrototypeOf(proto) } return Object.getPrototypeOf(obj) === proto }Copy the code

This function is used to determine if the object passed is pure (simple). Redux requires action and state to be pure

A simple Object(pure Object) is an Object created directly with Object literals {} or new Object() or object.create (null). Its constructor is Object

Ex: Object.getProtoTypeof object.getProtoTypeof () returns the prototype of the specified Object, Object.getProtoTypeof (Object)Copy the code

warning.js

export default function warning(message: string): void { /* eslint-disable no-console */ if (typeof console ! == 'undefined' && typeof console.error === 'function') { console.error(message) } /* eslint-enable no-console */ try { // This error was thrown as a convenience so that if you enable // "break on all exceptions" in your console, // it would pause the execution at this line. throw new Error(message) } catch (e) {} // eslint-disable-line no-empty }Copy the code

Uniformly handle errors encountered during code execution

The core file

createStore.js

1. Verify the parameters passed by the function

The first step in function execution is function validation. If the validation fails, an error is thrown. The first argument to the function is the mandatory function (reducers), the second argument to the function is the optional preloadState argument of any type other than the function, The third argument to the initial state is the optional function enhancer (an array of functions returned by the applyMiddleWare function). You can also pass two parameters reducers and enhancer

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. However, multiple middleware components must be wrapped in the compose function before they can be passed in. // The function first checks this. Determine if both the second and third arguments of a function are functions. Or the third and fourth arguments of the function are both functions. // The developer may be using compose to wrap multiple middleware components, so the function throws an error message. Indicates the possible cause of the current errorCopy the code
// If the default state is not passed (preloadedState for function type, enhancer for undefined type), then the passed preloadedState is enhancer. if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState // preloadedState will be assigned to enhancer if the second parameter is of type Function and the third parameter is undefined. //preloadedState will replace enhancer with undefined. if (typeof enhancer ! == 'undefined') { if (typeof enhancer ! == 'function') {// If enhancer is not null and not a function type, an error is reported. Throw new Error('Expected the enhancer to be a function.')} Note that enhancer is no longer passed here. // Enhancer actually handles createStore and returns an actual createStore to create a store object, see applymiddleware.js. Return enhancer(createStore)(Reducer, preloadedState)} // If reducer is not a function type, an error is reported. if (typeof reducer ! == 'function') { throw new Error('Expected the reducer to be a function.') }Copy the code

2. Define a series of local variables inside the function to store data

Let currentReducer = reducer // save the currentReducer. Let currentState = preloadedState // To store the current store, i.e. state. Let currentListeners = [] // Used to store all current subscribers registered through store.subscribe. Let nextListeners = currentListeners // New listeners array to ensure that no listeners are modified directly. Let isDispatching = false // Current state, preventing nested calls from reducer.Copy the code

Among them, the variable isDispatching is used as a lock. Our Redux is a unified managed state container, which needs to ensure the consistency of data, so we can only make one data modification at the same time. If two actions trigger the modification of the same data by the reducer at the same time, huge disasters will be caused. So the variable Isregularly exists to prevent this.

EnsureCanMutateNextListeners function

// Make sure that the nextListeners can be modified, if they point to the same array as the currentListeners // Make the nextListeners copies of the currentListeners. Prevent currentListeners from changing the nextListeners. function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } }Copy the code

⚠️ Why nextListeners are needed?

If the listener is called, the subscription or unsubscription will not take effect during the current dispatch() process. However, in the next dispatch() call, whether the dispatch is nested or not, The nextListener is added using the most recent snapshot of the listeners list to avoid missing listeners due to subscribe or unsubscribe changes made to the listeners

GetState function

CurrentState is updated with the response during each dispatch, and the function first judges whether the current dispatching is in the dispatching state. If yes, throw an error. Otherwise, the local variable current of the function is returned. To ensure data consistency, the current state value cannot be read when reducer operation is performed

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

The subscribe function

  • Determines whether the listener is a function
- Whether a reducer is modifying data (to ensure data consistency) // Store. Subscribe and subscribe dispatch. function subscribe(listener) { if (typeof listener ! == 'function') {throw new Error('Expected the listener to be a function.')} // No subscription allowed in reducer. 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-reference/store#subscribe(listener) for more details.')} // Define whether the local variable flag has been subscribed and marked as a true let Islisteners = true // To ensure that the nextListeners are the same as currentListeners // Ensure that they can be modified before each operation. EnsureCanMutateNextListeners () / / store the subscriber registration method. NextListeners. Push (listener) // Returns a function to write off the current subscriber. Return function unsubscribe() {return function unsubscribe() {// If the listener is still subscribed, then return 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-reference/store#subscribe(listener) for more details.' ) } Islisteners = false // Ensure that the process can be modified before each operation. EnsureCanMutateNextListeners () / / use an array of indexOf method in the current nextListeners array to find the corresponding index const index = Splice (index, 1)}}Copy the code

The dispatch function

// the store.dispatch function, which triggers an action to modify state. function dispatch(action) { if (! IsPlainObject (Action)) {// The action must be a pure object. throw new Error( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' ) } if (typeof Action. Type === 'undefined') {// Each action must contain a type attribute that specifies the type to be modified. throw new Error( 'Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant? } // No actions are allowed to be sent internally. If (isDispatching) {throw new Error('Reducers may not dispatch actions.')} Stop the following actions from triggering the reducer operation, // Assign the obtained state value to currentState, and then change isabled to false in finally. Allow subsequent actions to trigger the reducer operation try {// Before calling the reducer, mark true. IsDispatching = true // Call reducer, and the returned value is the latest state. CurrentState = currentReducer(currentState, action)} finally { Dispatch ends. IsDispatching = false} // After dispatch, the functions to execute all subscribers are executed in turn. Const listeners = (currentListeners = nextListeners) for (let I = 0; i < listeners.length; I ++) {const listener = listeners[I] listener()} // Returns the action currently in use. This step is important for middleware nesting. return action }Copy the code

ReplaceReducer function

Used to dynamically load the Reducer

Check whether the reducer is a function if (typeof nextReducer! == 'function') {throw new Error('Expected the nextReducer to be a function.')} To replace the reducer effect and trigger the state update operation. // TODO: do this more elegantly ; ((currentReducer as unknown) as Reducer< NewState, NewActions >) = nextReducer // This action has a similar effect to ActionTypes.INIT. // Any reducers that existed in both the new and old rootReducer // will receive the previous state. This effectively populates // the new state tree // Execute the default REPLACE action with any relevant data from the old one. dispatch({ type: ActionTypes.REPLACE } as A)Copy the code

Observables function

Subscription store changes slightly

Code that is easily overlooked, initialized

// Dispatch an action of type INIT during initialization to verify various cases

State ({type: actiontypes.init}); // We need to get all the state (reducer) default state, so that we can update our state dispatch({type: actiontypes.init}).

The Store object is a pure JavaScript object. Contains several property apis, and our state is stored inside the createStore method as a local variable that can only be accessed through the getState method

combineReducers.js

GetUndefinedStateErrorMessage function

A tool function to get error information. If a call to a Reducer you defined returns undefined, call this function to throw the appropriate error information.

Key Is the name of a Reducer function that you defined, which is also a property name of state

Action Action used when the Reducer was called

GetUnexpectedStateShapeWarningMessage function

Find out that there is no key corresponding to reducer in state, and prompt the developer to make adjustments

assertReducerShape

Check whether each reducer in finalReducers has a default return value

Function assertReducerShape(reducers) {object.keys (reducers). ForEach (key => {const reducer = reducers[key] // Obtain the reducers when initialized The state. const initialState = reducer(undefined, { type: Actiontypes.init}) // If you do not want to set the reducer value, Return null instead of undefined if (typeof initialState === 'undefined') {throw new Error(' Reducer "${key}" returned undefined during initialization. ` + `If the state passed to the reducer is undefined, you must ` + `explicitly return the initial state. The initial state may ` + `not be undefined. If you don't want to set A value for this reducer, '+' you can use null instead of undefined. ')} // If the reducer is reducer, it is possible that you defined actiontypes.init. At this point, the random value is checked again. // Check whether the reducer has been properly processed by unknown actions, that is, whether the value type returned is undefined, if so, error causes are thrown // If undefined is returned, it indicates that the user may have processed the INIT type, which is not allowed. if ( typeof reducer(undefined, { type: ActionTypes.PROBE_UNKNOWN_ACTION() }) === 'undefined' ) { throw new Error( `Reducer "${key}" returned undefined when probed with a random type. ` + `Don't try to handle ${ ActionTypes.INIT } or other actions in "redux/*" ` + `namespace. They are considered private. Instead, you must return the ` + `current state for any unknown actions, unless it is undefined, ` + `in which case you must return the initial state, regardless of the ` + `action type. The initial state may not be undefined, but can be null.` ) } }) }Copy the code

combineReducers

// Convert your defined reducers object into a large summary function. // As can be seen, combineReducers takes a reducers object as a parameter, and then returns a total function as the final legal reducer. This reducer accepts actions as parameters, Traverse all reducer calls based on the type of action. Export Default Function combineReducers(Reducers) {// Obtain all the attribute names of the reducers. Const reducerKeys = object. keys(reducers) const finalReducers = {} // Go through all attributes of the reducers and eliminate all invalid reducers. for (let i = 0; i < reducerKeys.length; I ++) {const key = reducerKeys[I] == 'production') { if (typeof reducers[key] === 'undefined') { warning(`No reducer provided for key "${key}"`) } } if (typeof Reducers [key] === 'function') {// Copy all reducers into the new finalReducers object. FinalReducers [key] = Reducers[key]}} // finalReducers is a pure filtered reducers, get all attribute names again. Const finalReducerKeys = object. keys(finalReducers) let unexpectedKeyCache // unexpectedKeyCache contains all states with buts Attributes not found in reducers. if (process.env.NODE_ENV ! == 'production') {unexpectedKeyCache = {}} let shapeAssertionError try {// Check all reducer rationality, cache error. AssertReducerShape Verifies the finalReducers type, And store the error information to the local variable shapeAssertionError assertReducerShape(finalReducers)} Catch (e) {shapeAssertionError = e} // This is the new one returned Reducer, a pure function. Execute the Combination function every time you dispatch an action, and // execute all the reducer you defined. Return function combination(state = {}, action) {// In the new function, first judge the shapeAssertionError, If (shapeAssertionError) {// If there are cache errors, throw. throw shapeAssertionError } if (process.env.NODE_ENV ! == 'production') {// In a non-production environment, verify whether all attributes in state have corresponding reducer. / / for informal environment, using getUnexpectedStateShapeWarningMessage for checking, And remind error const warningMessage = getUnexpectedStateShapeWarningMessage (state, finalReducers, action, UnexpectedKeyCache) if (warningMessage) {warning(warningMessage)}} let hasChanged = false // state Indicates whether the flag bit changes. Const nextState = {} // New state returned by reducer. for (let i = 0; i < finalReducerKeys.length; I++) {// traverse all reducer files. Const key = finalReducerKeys[I] // Obtain the Reducer name. Const reducer = finalReducers[key] // Obtain the reducer. Const previousStateForKey = state[key] // Old state value. Const nextStateForKey = Reducer (previousStateForKey, action) // New state[key] value returned after the reducer execution. If (typeof nextStateForKey === 'undefined') {// If (Typeof nextStateForKey === 'undefined') { const errorMessage = getUndefinedStateErrorMessage(key, NextState [key] = nextStateForKey action) throw new Error(errorMessage)} nextState[key] = nextState Forkey The reducer name you defined is the property of the corresponding state, so the Reducer name needs to be regulated! hasChanged = hasChanged || nextStateForKey ! == previousStateForKey // Check whether state has changed. } // Return the corresponding state according to the flag bit. return hasChanged ? nextState : state } }Copy the code

applyMiddleware.js

The applyMiddleware function is a middleware function that allows us to process actions before they reach the reducer. The return from applyMiddleware is an enhancer

Import compose from './compose' // a function for applying middleware that can deliver multiple middleware at the same time. The standard form of middleware is // const middleware = store => next => action => {/*..... */ return next(action); } export default function applyMiddleware(... Middlewares) {// Returns a function that takes createStore as an argument. The ARGS parameters are reducer and preloadedState. return createStore => (... Args) => {// Call createStore inside the function to create a store object, which does not pass enhancer because applyMiddleware itself creates an enhancer, It then calls createStore. // This actually delays the creation of a store by applyMiddleware. Why delay? // Because middleWares is used to initialize the middleware, redefine the dispatch, and then create the Store, // The store created in this case contains dispatch methods that are different from those created when enhancer is not passed. It contains some logic defined by the middleware, which is why the middleware can interfere with dispatch. const store = createStore(... Args) // Const store = createStore(Reducer, preloadedState) The purpose of this is to prevent your middleware from being initialized for execution validation, Dispaching let dispatch = () => {throw new Error(' Often while constructing your middleware is not Allowed. '+' Other Middleware would not be applied to this dispatch. ')} // middlewareAPI objects are created for middleware functions to use const middlewareAPI = { getState: store.getState, dispatch: (... args) => dispatch(... Args) // Note that the dispatch function will not be accessed during the final dispatch because it is overridden by the following dispatches. } // For each middleware block, the middlewareAPI is called, which is middleware initialization. // The initialized middleware returns a new function that takes store.dispatch as an argument and returns a replacement dispatch as the new // store.dispatch. Middlewares.map (Middleware => Middleware API) const chain = middlewares.map(Middleware => Middleware API) // middlewareAPI is then taken as an argument to facilitate the execution of middleware array functions. Store the returned result in the chain array // compose method and call all middleware in tandem. Replace the dispatch function with the final result, and all store.dispatch methods used thereafter have been // replaced with new logic. ////store.dispatch executes each function in the chain array as an argument, by executing the first function first and passing in store.dispatch as an argument. // Take the result it returns as an argument to the second function, and so on, assign the final result to dispatch = compose(... Chain)(store.dispatch) // Overwrite the dispatch function after initialization. /** * Standard form of middle: * const middleware = ({getState, dispatch}) => next => action => {* //.... * return next(action); *} * Here next is the dispatch method processed by the previous middleware. * Next (Action) still returns a dispatch method. */ return { ... Store, dispatch // Brand new dispatch. }}}Copy the code
  • Create a store using the createStore method
  • Defines dispatch to throw an error message if called during middleware construction
  • Define the middlewareAPI with two methods, getState and Dispatch, as a bridge to the store called by the middleware
  • Middlewares calls array.prototype. map and stores it in chain
  • Compose consolidates the chain array and assigns the value to dispatch
  • Replace the original store.dispatch with the new dispatch

The latest store values are stored through closures.

The compose function makes the next parameter of each middleware point to the middleware function that follows it.

The last piece of middleware points to Store.dispatch.

When an action is triggered, it is processed by the middleware in turn.

The latest state value can be obtained from store.getState() in each middleware, and dispatch() can be triggered from the first middleware through dispatch.

Trigger the next middleware function by calling next(Action)

bindActionCreators.js

Convert Action Creator into a function you can use directly

BindActionCreators has three return values for three scenarios

function bindActionCreator(actionCreator, dispatch) { return function() { return dispatch(actionCreator.apply(this, Arguments)}} // Take an actionCreator (or actionCreators object) and a dispatch function as parameters, and return a function or an object, Executing this function or the function in the object directly saves you from having to call Dispatch again. Export default function bindActionCreators(actionCreators, Dispatch) {// If actionCreators is a function and not an object, Call the bindActionCreators method directly for the conversion, in which case the return // result is also a function that will dispatch the corresponding action. if (typeof actionCreators === 'function') { return bindActionCreator(actionCreators, //actionCreators is neither a function nor an object, or is empty, and an error is thrown. if (typeof actionCreators ! == 'object' || actionCreators === null) { throw new Error( `bindActionCreators expected an object or a function, instead received ${ actionCreators === null ? 'null' : typeof actionCreators }. ` + `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"? ')} // If actionCreators were an object, each of its properties would be an actionCreator, iterated through each actionCreator, // convert with bindActionCreator. const keys = Object.keys(actionCreators) const boundActionCreators = {} for (let i = 0; i < keys.length; I ++) {const key = keys[I] const actionCreator = actionCreators[key] // Bind the conversion result to the boundActionCreators object, which will eventually be returned. if (typeof actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators }Copy the code

BindActionCreators (actionCreators, Dispatch) returns a new function identical to the original function/object, saves the Store. dispatch through the closure and adjusts the orientation of this through apply.

Directly executing the properties of the returned function/object triggers the data change, eliminating the need to pass the Dispatch layer by layer and triggering the action as if it were a normal function.

compose.js

Nested calls to middleware, initialization, combine multiple functions into a single function used in the source applyMiddleware function

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

Passing in a series of single-argument functions as parameters (funcs array) returns a new function that can take multiple arguments and at run time calls the funcs functions from right to left.

  • Create a new array funcs and copy each item from arguments into Funcs
  • When funcs has length 0, returns a function that returns what is passed in
  • When funcs has length 1, return the function corresponding to the 0th term of funcs
  • When funcs has a length greater than 1, the array.prototype. reduce method is called for consolidation