isDispatching
The createStore method in the Redux source code maintains a isDispatching variable, which represents the state of Dispatch. When dispatching is called, the variable is assigned to true, and the detailed code is
let isDispatching = false
function dispatch(action: A) {
// ...
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')}try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// ...
return action
}
Copy the code
So what does Redux need isDispatching to do, and where does he also use it? Searching this variable in the source code, we find that in addition to the Dispatch function itself isDispatching is used, we also find that isDispatching is consumed in the getState, SUBSCRIBE, and unsubscribe methods, whose logic is similar. That is, an error is raised when isDispatching is true. The code for
// subscribe unsubscribe
function subscribe(listener: () => void) {
if (typeoflistener ! = ='function') {
throw new Error('Expected the listener to be a function.')}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.')}// ...
return function unsubscribe() {
// ...
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.')}// ...}}// getState
function getState() :S {
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.')}return currentState as S
}
Copy the code
IsDispatching seems to have no effect. We know that JS is a single-threaded language, and dispatch, getState, SUBSCRIBE, and unsubscribe are all synchronous functions. Since it is a synchronous scene, when we call Dispatch, Js will execute this function and then process other functions. Dispatch and getState, SUBSCRIBE and unsubscribe are not executed at the same time. But the problem lies in the dispatch function itself, so let’s take a closer look at the dispatch core implementation.
function dispatch(action: A) {
// ...
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false}}Copy the code
Dispatch sends actions to the reducer. CurrentReducer here is the Reducer passed in by createStore, that is, the reducer compiled by the developer. Let’s have a reducer:
const reducer = (state={}, action) = > {
store.dispatch({type: 'SOMEACTION'})
return state
}
Copy the code
The dispatch method is also called inside the reducer. If isDispatching is not present, the infinite loop call of dispatch -> reducer -> dispatch -> Reducer occurs during the dispatch execution, resulting in stack overflow. Therefore, Redux believed that the reducer received from the outside was unsafe, and limited the reducer through isDispatching.
CurrentListeners and nextListeners
Redux implements the subscription-listening-publish function. By subscribing to the subscribe method, Redux adds a listener listener and notifies all listeners every time it calls Dispatch. The normal code for this function is as follows:
// Simple code
const listeners = []
function subscribe(listener) {
listeners.push(listener)
return function unsubscribe() {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)}}function dispatch() {
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
}
Copy the code
However, currentListeners and nextListeners are internally maintained by Redux
let currentListeners = []
let nextListeners = currentListeners
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
function subscribe(listener) {
// ...
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
// ...
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null}}function dispatch(action) {
// ...
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
Copy the code
In fact, the SUBSCRIBE method receives external listeners. If the listener executes a SUBSCRIBE or unsubscribe method, the listener execution will be chaotic or even error. Subscribe like this:
function loopSubscribe () {
store.subscribe(loopSubscribe)
}
loopSubscribe()
store.dispatch()
Copy the code
To prevent an incoming listener from calling a SUBSCRIBE listener, Redux uses two variables to maintain the listener. Subscribe and unsubscribe are methods that operate on the Listeners and merge the nextListeners into the currentListeners. Iterate over all currentListeners. In other words, dispatch takes a listener snapshot, and if the listener executes a SUBSCRIBE or unsubscribe method, it only takes effect the next time the Dispatch method executes.
applyMiddleware
The Redux ecosystem relies on middleware, of which Redux-Thunk is one of many middleware with the following code:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) = > (next) = > (action) = > {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
Copy the code
Redux-thunk seems relatively complex, returning three layers of functions in turn. Let’s look at how applyMiddleware, the core function of Redux middleware, handles middleware.
function applyMiddleware(. middlewares) {
return createStore= > {
const store = createStore(reducer, preloadedState)
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: (action, ... args) = >dispatch(action, ... args) }const chain = middlewares.map(middleware= >middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch)return {
...store,
dispatch
}
}
}
Copy the code
Redux unlocks the three layers of middleware functions in turn. The first layer of Redux traverses all of the middleware and empowers the middlewareAPI, which has a dispatch method. However, the dispatch is not the original store dispatch, and it will be modified later.
At this point, the middleware is handed over to Compose for a chain combination. The result is to give each middleware the ability to call the next middleware (next), which is the second layer.
Compose’s implementation is also clever:
function compose(. funcs) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return arg= > arg
}
if (funcs.length === 1) {
return funcs[0]}return funcs.reduce((a, b) = > (. args) = >a(b(... args))) }Copy the code
If there are two middleware components a, B, the compose result is **(… args) => a(b(… Args))**, compose returns the result to store.dispatch, so that middleware A can call next to execute middleware B, and middleware B can call next to execute store.dispatch.
The final layer is where the developer calls Dispatch to pass in the action, and the third layer functions access the Action object.
Note that the last middleware call to Next is a call to Store. dispatch, which is an unenhanced dispatch.
The end of the
Although Redux is a little “outdated” at present, there are still lessons in the design and details, such as isDispatching lock, listener snapshot and other implementations, which help us produce more robust code.