Redux: ask yourself and answer yourself

Time:2021-9-17

I read the source code of Redux some time ago and wrote an analysis on the source code of Redux:Redux: one hundred lines of code and one thousand lines of document, you can have a look. The whole article mainly analyzes the operation principle of Redux, but there are many contents that have not been explicitly studiedWhy did you do that?The main content of this article is that I put forward some questions myself, then try to answer this question, make an advertisement again, and welcome everyone to pay attention to meNuggets accountAnd mineBlog
  

Why do both currentlisteners and nextlisteners exist in createstore?

Students who have read the source code should understand,createStoreFunction to savestoreThe current subscriber is not only savedcurrentListenersAnd it’s preservednextListenerscreateStoreThere is an internal function inensureCanMutateNextListeners:
  

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

The essence of this function is to ensure that it can be changednextListeners, ifnextListenersAndcurrentListenersIf consistent, it willcurrentListenersMake a copy assignment tonextListenersThen all operations will focus onnextListenersFor example, let’s look at the subscription functionsubscribe:

function subscribe(listener) {
// ......
    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
        // ......
        ensureCanMutateNextListeners()
        const index = nextListeners.indexOf(listener)
        nextListeners.splice(index, 1)
}

We found that both subscription and unsubscribe are innextListenersDo the operation, and then each timedispatchOneactionWill do the following:

function dispatch(action) {
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
    //Equivalent to currentlisteners = nextlisters const listeners = currentlisteners
    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    return action
  }

We found thatdispatchI did itconst listeners = currentListeners = nextListeners, equivalent to updating the currentcurrentListenersbynextListenersThen inform the subscriber that we can’t help asking hereWhy does this existnextListeners?
  
In fact, the comments in the code also provide relevant explanations:

The subscriptions are snapshotted just before every dispatch() call.If you subscribe or unsubscribe while the listeners are being invoked, this will not have any effect on the dispatch() that is currently in progress.However, the next dispatch() call, whether nested or not, will use a more recent snapshot of the subscription list. 

Let me translate this scum who hasn’t passed level 6: subscribers every timedispatch()The call is preceded by a snapshot. If you arelistenerDuring the call, subscribe or unsubscribe in thisdispatch()It will not take effect in the process, but in the next timedispatch()Call, regardless ofdispatchWhether it is nested or not, the most recent snapshot subscriber list will be used. The effects shown in the figure are as follows:
  
Redux: ask yourself and answer yourself  
  
We can see from this diagram that if this does not existnextListenersThis snapshot, becausedispatchCausedstoreIf other subscriptions and unsubscribes occur in the process of notifying subscribers, it will certainly happenerrorperhapsUncertainty。 For example, in the process of notification subscription, if unsubscribe occurs, it is possible to unsubscribe successfully (implemented before notification)nextListeners.splice(index, 1))Or unsuccessfully unsubscribe (implemented after notificationnextListeners.splice(index, 1)), of course not. becausenextListenersTherefore, the behavior of notifying subscribers is clear, and subscription and unsubscribe will not affect the process of subscriber notification.

There is no problem, but there is a problem. Isn’t JavaScript single threaded? How can the scene mentioned above occur? Unable to figure it out, I opened an issue under the Redux project and got the answer from the maintainer:

Redux: ask yourself and answer yourself  

Come on, let’s take a look at the test related code. After reading it, I learned. Indeed, because JavaScript is a single threaded language, it is impossible to have the multi-threaded scenario mentioned above, but I have ignored one point. When executing the subscriber function, unsubscribe or subscribe events can be executed in this callback function. For example:

const store = createStore(reducers.todos)
const unsubscribe1 = store.subscribe(() => {
    const unsubscribe2 = store.subscribe(()=>{})
})

This is not to mix subscriptions in the process of notifying the listenersubscribeAnd unsubscribeunsubscribeAre you?
  

Why can’t dispatch be performed in reducer?

We know wherereducerCannot execute in functiondispatchOperational. one side,reducerAs the next calculationstatePure functions should not assume executiondispatchSuch an operation. On the other hand, even if you try toreducerMedium executiondispatch, it will not succeed, and you will get a prompt of “reducers may not dispatch actions.”. Because indispatchThe function makes relevant restrictions:
  

function dispatch(action) {
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    //...notice listener
}

In executiondispatchThe flag bit will beisDispatchingSet astrue。 And then ifcurrentReducer(currentState, action)In the process of execution, thedispatch, an error (‘reducers may not dispatch actions. ‘) will be thrown. The reason for this restriction is that indispatchWill causereducerIf at this timereducerIt was executed again in thedispatchIn this way, it falls into an endless cycle, so it should be avoidedreducerMedium executiondispatch

Why does dispathc in the middleware API in applymiddleware need to be wrapped with closures?

I wrote a related article about Redux middleware beforeRedux: middleware, why are you so hard, students who haven’t seen it can learn about it. In fact, there is one place in the article that has not been clearly explained. At that time, beginners didn’t understand it very well. Now let’s explain:
  

export default function applyMiddleware(...middlewares) {            
    return (next)  => 
        (reducer, initialState) => {

              var store = next(reducer, initialState);
              var dispatch = store.dispatch;
              var chain = [];

              var middlewareAPI = {
                getState: store.getState,
                dispatch: (action) => dispatch(action)
              };

              chain = middlewares.map(middleware =>
                            middleware(middlewareAPI));
              dispatch = compose(...chain, store.dispatch);
              return {
                ...store,
                dispatch
              };
           };
}

The problem is why dispathc in the middleware API should be wrapped with closures instead of being passed in directly? First, use a diagram to explain the middleware:
  
  Redux: ask yourself and answer yourself
  
As shown in the figure above, the execution process of middleware is very similar to onion rings. Suppose we are in the functionapplyMiddlewareThe order of incoming Middleware in is mid1, mid2 and mid3 respectively. The structure of middleware functions is similar to:

export default function createMiddleware({ getState }) {
    return (next) => 
        (action) => {
            //before
            //......
            next(action)
            //after
            //......
        };
}

Then the internal code execution order of middleware functions is:
  
Redux: ask yourself and answer yourself

But if you call it in the middleware function,dispatch(use)mid3-beforeFor example), the order of execution becomes:

Redux: ask yourself and answer yourself  

So it is passed to the middleware functionmiddlewareAPIindispatchFunction is passedapplyMiddlewareTransformeddispatch, notreduxNativestore.dispatch。 So we wrapped it through a closuredispatch:

(action) => dispatch(action)

So we’ll give it backdispatchAssign asdispatch = compose(...chain, store.dispatch);In this way, as long as the dispatch is updated, the dispatch application in the middleware API will also change. If we write:

var middlewareAPI = {
    getState: store.getState,
    dispatch: dispatch
};

That’s what’s accepted in the middleware functiondispatchAlways can only be the beginningreduxMediumdispatch

Finally, if you have other doubts and feelings when reading the Redux source code, you are welcome to communicate, discuss and learn from each other in the comment area.

Recommended Today

Seven Python code review tools recommended

althoughPythonLanguage is one of the most flexible development languages at present, but developers often abuse its flexibility and even violate relevant standards. So PythoncodeThe following common quality problems often occur: Some unused modules have been imported Function is missing arguments in various calls The appropriate format indentation is missing Missing appropriate spaces before and after […]