Before the introduction

Hello friends, I am Jarvis from the Push ah front end team. This time I will share the content of “interpretation of Redux, React-Redux source code”. If you have a different opinion, please comment on it in the comments section ~ 😝😝😝

Writing in the front

As React’s original “state management repository,” Redux has always been a controversial presence, with some people loving its advantages of a single data flow and easy management of backtracking, while others hate the complexity of its introductory demo. But overall, Redux is still an impressively smart JS library. As a deep user of React, I have a lot to share.

After experiencing Redux for the first time, I had the following questions in mind:

  1. reducertheswitchWhy are statements writtendefaultBranches, what’s the problem if I don’t write them? πŸ€”
    const counter = (state, action) = > {
      switch (action.type) {
        case 'ADD':
          return state + 1;
        // I don't want to write
        // default:
        // return state;}}Copy the code
  2. Redux wasn’t enough? Why the React-Redux? 🧐
  3. Components don’t writeconnect.storeupdateNot triggerComponent updates, according to? 🀨
  4. Why useVuexWhen the amount of code is so small, and useReduxSo much code to write? 🀨
  5. I want toactionWith asynchronous methods, why use middleware? 🀨
  6. Middleware is a hammer? 🀨
  7. How do you read Redux? Why did someone teach me durex[‘ DJ ΚŠΙ™reks] 🀣

Maybe you’re easy to talk about, or maybe you’re confused too. In fact, for this kind of problem, the most fundamental solution is to understand the principle, can clear the fog and see the light.

Next we will combine the source code, Redux and React-Redux principle to explore 🧐🧐🧐.

Relationship between the two

Before we talk about the source code, let’s talk about the relationship between the two,

Redux is a js based data warehouse that defines actions when data changes through manual subscription updates.

If there were no React-redux, we would use Redux in this situation:

import { createStore } from 'redux';

// 1. Create the Reducer
function counter(state, action) {
  switch (action.type) {
    case 'ADD':
      return state + 1;
    default:
      returnstate; }}// 2. Create a store
const store = Redux.createStore(counter);

// 3. Use in application
function App() {
  const [, forceUpdate] = useState({});
  useEffect(() = > {
    const unsubscribe = store.subscribe(() = > {
      forceUpdate({});
    });

    return () = >unsubscribe(); } []);const add = () = > {
    store.dispatch({
      type: 'ADD'}); };return <div onClick={add}>{store.getState()}</div>;
}
Copy the code

Huh? Even manually subscribe to update, is it very troublesome πŸ€” ~


We want to simplify the code, after all, writing subscription updates in components is too intrusive for our business, so we found React-Redux. React-redux is the bridge between React and Redux, which simplifies the cost of using Redux in React. Make it easy for us to get the store we want in the React component and automatically subscribe to updates, so our actions will be simple:

import { createStore } from 'redux';

// 1. Create the Reducer
function counter(state, action) {
  switch (action.type) {
    case 'ADD':
      return state + 1;
    default:
      returnstate; }}// 2. Create a store
const store = Redux.createStore(counter);

// 3. Share the status in the root application
ReactDom.render(
  <Provider store={store}>
    <App />
  </Provider>.document.getElementById('root'))// 4. Use in business components
const App = connect()((props) = > {
  const add = () = > { props.dispatch({ type: 'ADD'}); };return <div onClick={add}>{store.getState()}</div>;
})
Copy the code

As you can see, we no longer need to subscribe to updates manually, and components processed through the connect method now have new props. This is actually the core part of react-Redux.

Redux is not a data warehouse specifically designed for React. It can be used in any JS library.

1. Redux

1.1. Know in advance

  • Functional programming
    • Pure functions
    • Compose function
    • Currie,
  • The onion model

1.2. Source code (1) : createStore

1.2.1. createStore

  • role

    • Creating a data warehousestore
  • The core source

    // Some code is omitted
    export default function createStore(reducer, preloadedState, enhancer) {
      // Handle the edge case./ / middleware
      if (typeofenhancer ! = ='undefined') {
          return enhancer(createStore)(reducer, preloadedState)
      }
    
      // Save the Reducer, because there may be overwriting operations
      let currentReducer = reducer;
      // Store the current state
      let currentState = preloadedState;
      // Store the subscription function
      let currentListeners;
      // Store the next batch of subscription functions
      let nextListeners = currentListeners;
      // Whether the Reducer is being executed
      let isDispatching = false;
    
      // Deep copy currentListeners to nextListeners
      function ensureCanMutateNextListeners() {}
    
      // Get the current state
      function getState() :S {}
    
      // Add a subscription function
      function subscribe(listener: () => void) {}
    
      // Call Reducer to generate new state
      function dispatch(action: A) {}
    
      / / replace reducer
      function replaceReducer(nextReducer);
    
      // Extensible methods for observer pattern/responsive frameworks, such as Vue
      function observable() {}
    
      // Initialize the state
      dispatch({ type: ActionTypes.INIT } as A);
    
      const store = {
        dispatch: dispatch as Dispatch<A>,
        subscribe,
        getState,
        replaceReducer,
        [$$observable]: observable,
      };
      return store;
    }
    Copy the code

    A Store instance consists of five parts,

    • getState: Obtains the current status
    • dispatch: callreducerGenerate a newstate
    • subscribe: Adds a subscription function
    • ensureCanMutateNextListeners: Deep copy currentListenersAssigned tonextListeners
    • observable: in order toObserver mode/responsiveExtension methods provided by the framework

    Note that the type of action (actiontypes.init) is a concatenated random string:

    const ActionTypes = {
      INIT: `@@redux/INITThe ${/* #__PURE__ */ randomString()}`.// There are two others, also with random numbers
    };
    Copy the code

    Type is a random string. If you go to the default branch of the switch statement, the return result will be the initial value of state. Therefore, not handling the default branch may cause some errors πŸ’¦πŸ’¦πŸ’¦.

1.2.2. getState

  • role

    • Returns the currentstate
  • The core source

    function getState() {
      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;
    }
    Copy the code

    IsDispatching is the labels that are executing the Reducer. The Reducer is used to change state. Therefore, it is not safe to obtain the state at this time.

1.2.3. dispatch

  • role

    • throughreducerchangestate
    • Trigger all subscription updates
  • The core source

    function dispatch(action: A) {
      // Handle both edge cases and make sure the action parameter is available.try {
        isDispatching = true;
        currentState = currentReducer(currentState, action);
      } finally {
        isDispatching = false;
      }
    
      const listeners = (currentListeners = nextListeners);
      for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i];
        listener();
      }
    
      return action;
    }
    Copy the code

    We take the action as an argument and return a new state via currentReducer, which is the reducer we write to.

    Finally, the action is returned as is for later use by the middleware, which is explained in detail in the middleware section.

    We see: return a new state each time. Think about the unnecessary rendering if you return a new reference every time the component is not optimized. πŸ˜–πŸ˜–πŸ˜– Remember, this is a big hole! We’ll sort it out at the end.

1.subscribe

  • role

    • Adding a subscription function
  • The core source

    function subscribe(listener: () => void) {
      // In the edge case, ensure that the listener is a function and the reducer is not executing.let isSubscribed = true;
    
      // Add to nextListeners, where the subscription functions are stored
      ensureCanMutateNextListeners();
      nextListeners.push(listener);
    
      return function unsubscribe() {
        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;
    
        ensureCanMutateNextListeners();
    
        // Delete the current subscription function
        const index = nextListeners.indexOf(listener);
        nextListeners.splice(index, 1);
        currentListeners = null;
      };
    }
    Copy the code

    Adds a subscriber and returns a method to unsubscribe. What is the ensureCanMutateNextListeners here?

1.2.5. ensureCanMutateNextListeners

  • role

    • Ensure that no changes are made to the currently available subscription functions
  • The core source

    let currentListeners = [];
    let nextListeners = currentListeners;
    function ensureCanMutateNextListeners() {
      if(nextListeners === currentListeners) { nextListeners = currentListeners.slice(); }}function subscribe(fn) {
      ensureCanMutateNextListeners(); // A deep copy is made here
      nextListeners.push(fn); // Adding to the nextListeners array does not affect the currentListeners array
    }
    Copy the code

    I was confused when I first saw this, but after many “debugs”, I finally found that this is to deal with some edge situation: if you delete or add a subscriber during the update, this update will not include the new subscriber, and will be carried in the next update.

1.3. Source code (2) : middleware

The reducer is a simple calculator that receives the state and action and returns the new state.

As you can imagine, we can’t change state directly in dispatch using asynchronous methods.

Next comes middleware, which enhances Dispatch πŸ“

constStore = createStore(Reducer, applyMiddleware)1, the middleware2, the middleware3,...). )Copy the code

1.3.1. applyMiddleware

  • role

    • Receive multiple middleware as parameters
    • The middleware is invoked in order
    • And finally return a new dispatch (enhanced Dispatch)
  • The core source

    export default function applyMiddleware(. middlewares) {
      return (createStore) = > (reducer, preloadedState) = > {
        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

    We’re using a functional programming concept called compose, which is the Onion ring model. We’ll look at the source code later, but we can simply think of it as getting an “enhanced Dispatch” that executes additional middleware logic each time it’s executed.

    Let’s start with the steps for applyMiddleware:

    1. First of all, in accordance with theThe orderforThe middlewareInto the corestoreAnd the corestoreincludinggetStateanddispatch;
    2. Then execute in orderThe middleware, which in turn returns the latestcreateStoreMethods;
    3. Then the dispatch is synthesized into an enhanced Dispatch through the chain-type composite middleware.
    4. And finally as a completestoreTo return.

    So the middleware is a higher-order function: it receives the store and returns the createStore function that returns a new dispatch

    Below is the source code for two commonly used middleware, you can see that both are very concise

    • redux-thunk

      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 three-tier function is returned because of a feature called Redux-Thunk, which allows us to dispatch a function for asynchronous operations.

    • redux-promise

      export default function promiseMiddleware({ dispatch }) {
        return (next) = > (action) = > {
          if(! isFSA(action)) {return isPromise(action) ? action.then(dispatch) : next(action);
          }
      
          return isPromise(action.payload)
            ? action.payload
                .then((result) = >dispatch({ ... action,payload: result }))
                .catch((error) = >{ dispatch({ ... action,payload: error, error: true });
                  return Promise.reject(error);
                })
            : next(action);
        };
      }
      Copy the code

      This one is more conventional, returning a nested two-level function that handles asynchronous situations.

1.3.2. compose

  • role

    • Combine multiple functions in order to form a new function
  • The core source

    export default function compose(. funcs:Function[]) {
      // The following two ifs deal with edge cases
      if (funcs.length === 0) {
        return <T>(arg: T) = > arg;
      }
    
      if (funcs.length === 1) {
        return funcs[0];
      }
    
      // Use reduce to combine functions in order
      return funcs.reduce((a, b) = > (. args:any) = >a(b(... args))); }Copy the code

    It is commonly used in functional programming to combine multiple functions. For example, compose(a, b, c), and you end up with a new function (… arbs) => a(g(c(… The args))). It can be simply understood as: The inner core of an onion is our dispatch. The so-called combination is to add layers of onion rings. Each onion ring is a middleware.

1.4. Source code (3) : Combinator

The combinator is the combineReducers

  • role

    • Receive multiplereducerAs a parameter
    • Finally return a new (enhanced) reducer
    • The newreducerIt goes through each one in turnreducerCalculate, and finally return the new state
  • The core source

    export default function combineReducers(reducers) {
      const reducerKeys = Object.keys(reducers);
      const finalReducers = {};
      // Retain the reducer that meets the specification
      for (let i = 0; i < reducerKeys.length; i++) {
        const key = reducerKeys[i];
    
        if (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key]; }}// End up with all the reducer keys
      const finalReducerKeys = Object.keys(finalReducers);
    
      // Return a new Reducer
      return function combination(state = {}, action) {
        // Use the hasChanged variable to record whether the state was changed before and after
        let hasChanged = false;
        // Declare an object to store the next state
        const nextState = {};
    
        / / traverse finalReducerKeys
        for (let i = 0; i < finalReducerKeys.length; i++) {
          const key = finalReducerKeys[i];
          const reducer = finalReducers[key];
          const previousStateForKey = state[key];
          / / reducer for execution
          const nextStateForKey = reducer(previousStateForKey, action);
    
          // Some code that handles edge cases. nextState[key] = nextStateForKey;// If two key comparisons are not equal, changehasChanged = hasChanged || nextStateForKey ! == previousStateForKey; }// The last keys array comparison is not equalhasChanged = hasChanged || finalReducerKeys.length ! = =Object.keys(state).length;
        return hasChanged ? nextState : state;
      };
    }
    Copy the code

    Take all the reducers and dispatch them from top to bottom, so action. Type must not be repeated!

becauseThis functionIs the function ofCombination reducersSo in the endreturnA newreducer

1.5. Process combing

  1. ReduxWhen initializing, it first determines whether middleware is used. If no middleware is used, it initializes by default. If middleware is used, it passesapplyMiddlewareFunction to create
  2. applyMiddlewareThe receivedparameterIs the defaultcreateStoreMethod, which will eventually return a newcreateStoreMethod, in whichdispatchThe function will beThe middlewareTo enhance
    • In 2.1.applyMiddlewareInside,The middlewareExecute the command in sequence to obtainMiddleware functionArray form of
    • 2.2. Then passcomposeAll of theMiddleware functionMerge into onedispatch
    • 2.3. The final will beTo strengthenthedispatchAnd the rest of thestoreBack together
  3. combineReducersTo accept an object type as an argument, it will be multiplereducerCombine into a function and executedispatchFrom the top down through each of themreducerGet the finalstate

2. React-Redux

The react-Redux principle is simple, but the source code is slightly complicated to handle many edge cases. Extracting the core can be summarized as follows

  1. Create a Context and use the Provider to share the Store with the child components.
  2. Class componentsthroughHigher-order functionsconnectComplete the subscription update; throughmapStateToPropsImplement props injection and passmapDispatchToPropsImplementation of dispatch injection;
  3. Function componentthroughuseSelectorComplete subscription update and implement state injection; throughuseDispatchReturn to the latestdispatch.

2.1. The connect

  • role

    • connectIs a high-level component, done inside the componentTo subscribe to, implementation,mapStateToProps ε’Œ mapDispatchToProps
  • Core principle code

    export const connect = (mapStateToProps, mapDispatchToProps) = > (
      WrapperComponent,
    ) = > (props) = > {
      const store = useStore();
      const { getState, dispatch } = store;
    
      const forceUpdate = useForceUpdate();
    
      // Client rendering using useLayoutEffect
      useLayoutEffect(() = > {
        const unSubscribe = store.subscribe(() = > {
          forceUpdate();
        });
        return () = >unSubscribe(); } []);const stateToProps = mapStateToProps(getState());
      let actionToProps = { dispatch };
    
      if (typeof mapDispatchToProps === 'object') {
        actionToProps = bindActionCreators(mapDispatchToProps, dispatch);
      } else if (typeof mapDispatchToProps === 'function') {
        actionToProps = mapDispatchToProps(dispatch);
      }
    
      return <WrapperComponent {. props} {. stateToProps} {. actionToProps} / >;
    };
    
    const useForceUpdate = () = > {
      const [, forceRender] = useReducer((s) = > s + 1.0);
    
      return forceRender;
    };
    Copy the code

    Implement mapStateToProps source and mapDispatchToProps or more complex, through the match and the factory function mapStateToPropsFactories, mapDispatchToPropsFactories completed, But the core code is what the above code looks like, with bindActionCreators as follows

    function bindActionCreators(actionMap, dispatch) {
      const actions = {};
      for (const action in actionMap) {
        actions[action] = bindActionCreator(actionMap[action], dispatch);
      }
      return actions;
    }
    
    function bindActionCreator(action, dispatch) {
      return (. args) = >dispatch(action(... args)); }Copy the code

    It’s worth noting in two ways,

    • 1. Implement forceUpdate function component, which is also recommended on the website.

      const [, forceRender] = useReducer((s) = > s + 1.0);
      Copy the code
    • 2. Why use useLayoutEffect? The source code makes a judgment here, server rendering use useEffect; Use useLayoutEffect for client rendering.

      In React, useLayoutEffect componentDidMount and componentDidUpdate are executed synchronously. UseEffect belongs to asynchronous execution, that is, after the end of this update phase, it is executed in the next task schedule.

      This means that if a client uses useEffect to subscribe during rendering, the subscription will be executed after the update task is complete, and any actions that trigger the update in the current update task will be lost.

      There are two reasons why the server uses useEffect for rendering. One is that the server uses useLayoutEffect to render and it will give a warning. Second, dom already exists when the server renders, and js may still be loaded at this time, so there is no delay in scheduling.

2.2. UseSelector and useDispatch

  • role

    • useSelectorin-houseTo subscribe toTo return the desiredstate
    • useDispatchReturn to the latestdispatch
  • Core principle code

    export const useSelector = (selector) = > {
      const store = useStore();
      const forceUpdate = useForceUpdate();
    
      useLayoutEffect(() = > {
        const unSubscribe = store.subscribe(() = > {
          forceUpdate();
        });
        return () = >unSubscribe(); } []);const ret = selector(store.getState());
      return ret;
    };
    export const useDispatch = () = > {
      const store = useStore();
      return store.dispatch;
    };
    
    const useStore = () = > {
      const store = useContext(Context);
      return store;
    };
    Copy the code

3. Think and summarize

  1. Redux and Vuex

    • Vuex relies on Vue, although it is implemented in a clever way, but it cannot be used without Vue; Redux is a Javascript library that can be used anywhere;
    • Vuex is extended through plug-ins, and multiple plug-ins can be extended with the onion model; Redux extends through middleware;
    • VuexBasically no difficulty to get started;ReduxNeed to knowFunctional programmingFor examplecompose,The onion model,Pure functionsAnd so on;
    • VuexOf the update granularityVue, belongs to directional update; whileReduxUpdate granularity benchmarking based on publish-subscribeReact, but need to useconnectAfter processing can be implementedAutomatic updates.
  2. Some notes

    • reducerIt’s a pure function, so don’t put any bands in itSide effectsSuch as publish subscribe;
    • mapStateToPropsandmapDispatchToPropsBoth have a second argument[ownProps]If this parameter is specified, the component will listen for changes to the Redux store. Otherwise, it will not listen. OwnProps is the current component’s props. MapStateToProps and mapDispatchToProps will be recalculated. Use this with caution!
  3. Meaning of the Default branch in the Reducer function

    When Redux is initialized, it calls the Init level Dispatch once to initialize the store. In this case, it uses the default branch of the switch statement. If it is not written, the default value of store will be undefined.

  4. The pros and cons of a single data source

    Perhaps a single data source is really easy to manage and easy to go back to. But there are a number of pitfalls with returning a new state each time. Imagine: We send a dispatch to get a new state. React doesn’t care if the actual contents of the current state have changed. It only sees the difference between the two references and updates. Redux is a lot like React in this respect, but a single reference data warehouse like Vuex or Mobx performs better.

  5. UseEffect and useLayoutEffect

    • UseEffect: During React rendering, changing the DOM, adding subscriptions, setting timers, logging, and performing other actions that contain side effects in the body of a function component are not allowed, as they can create unexplained bugs and break UI consistency. Use useEffect for side effects. Functions assigned to useEffect are delayed until the component is rendered to the screen. Think of Effect as an escape route from React’s purely functional world to the imperative world.

    • UseLayoutEffect: Its function signature is the same as useEffect, but it calls Effect synchronously after all DOM changes. You can use it to read the DOM layout and trigger rerendering synchronously. The code inside the useLayoutEffect is executed synchronously before the browser performs the drawing. Use standard useEffect whenever possible to avoid blocking visual updates.

Redux, React-Redux source code