抹桥的博客
Language
Home
Archive
About
GitHub
Language
主题色
250
3886 words
19 minutes
Redux Source Code Deep Dive (Long Read)

Redux’s source code is incredibly concise, providing powerful functionality in just a few hundred lines. Today, we’ll take a deep dive into it.

The simplest way to understand source code is to start from the entry file, observe its module dependencies, and then examine the content of each module in sequence. This approach ultimately provides a clear understanding of the entire codebase.

So let’s start by looking at the entry file:

import applyMiddleware from "./applyMiddleware";
import bindActionCreators from "./bindActionCreators";
import combineReducers from "./combineReducers";
import compose from "./compose";
import createStore from "./createStore";
import warning from "./utils/warning";

/*
 * This is a dummy function to check if the function name has been altered by minification.
 * If the function has been minified and NODE_ENV !== 'production', warn the user.
 */
function isCrushed() {}
// 就是根据 isCrushed 是否被压缩了,来警告开发者正在非生产环境使用一个压缩过的代码。
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 looseenvify (https://github.com/zertosh/looseenvify) for browserify " +
      "or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) " +
      "to ensure you have the correct code for your production build.",
  );
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
};

As you can see, it depends on the following modules:

  • createStore
  • combineReducers
  • bindActionCreators
  • applyMiddleware
  • compose
  • warning

There’s not much else to say, other than it exposes some APIs. So, let’s interpret them one by one, following this module dependency order.

createStore#

First up is createStore, used to create the application’s store. Its dependent modules are all utility functions.

  • isPlainObject
export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

The logic here is straightforward:

The first if statement means that if only two arguments are passed, and the second argument preloadedState is a function, then the second argument is considered an enhancer.

The second if statement ensures that enhancer is a function. When enhancer is passed as an argument, it returns enhancer(createStore)(reducer, preloadedState) as the return value of createStore, which is our desired store.

The third if statement ensures that reducer is a function.

Let’s continue:

let currentReducer = reducer;
let currentState = preloadedState;
let currentListeners = [];
let nextListeners = currentListeners;
let isDispatching = false;

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

/**
 * Reads the state tree managed by the store.
 *
 * @returns {any} The current state tree of your application.
 */
function getState() {
  return currentState;
}

Here, preloadedState is assigned to currentState. This allows the application to directly restore a specific state, or it can be used in server-side rendering where the initial state is computed by the backend.

The ensureCanMutateNextListeners function copies currentListeners to nextListeners when nextListeners === currentListeners is true. Its exact purpose isn’t entirely clear yet, so let’s set it aside for now.

Then, a method to get the current state is defined.

subscribe#

Next is the subscribe method.

/**
 * Adds a change listener. It will be called any time an action is dispatched,
 * and some part of the state tree may potentially have changed. You may then
 * call `getState()` to read the current state tree inside the callback.
 *
 * You may call `dispatch()` from a change listener, with the following
 * caveats:
 *
 * 1. 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.
 *
 * 2. The listener should not expect to see all state changes, as the state
 * might have been updated multiple times during a nested `dispatch()` before
 * the listener is called. It is, however, guaranteed that all subscribers
 * registered before the `dispatch()` started will be called with the latest
 * state by the time it exits.
 *
 * @param {Function} listener A callback to be invoked on every dispatch.
 * @returns {Function} A function to remove this change listener.
 */
function subscribe(listener) {
  if (typeof listener !== "function") {
    throw new Error("Expected listener to be a function.");
  }

  let isSubscribed = true;

  ensureCanMutateNextListeners();
  nextListeners.push(listener);

  return function unsubscribe() {
    if (!isSubscribed) {
      return;
    }

    isSubscribed = false;

    ensureCanMutateNextListeners();
    const index = nextListeners.indexOf(listener);
    nextListeners.splice(index, 1);
  };
}

The comments already explain it very clearly: register a listener function, push it to the current nextListeners list, and return an unsubscribe method to unregister this listener function.

dispatch#

function dispatch(action) {
  if (!isPlainObject(action)) {
    throw new Error(
      "Actions must be plain objects. " +
        "Use custom middleware for async actions.",
    );
  }

  if (typeof action.type === "undefined") {
    throw new Error(
      'Actions may not have an undefined "type" property. ' +
        "Have you misspelled a constant?",
    );
  }

  if (isDispatching) {
    throw new Error("Reducers may not dispatch actions.");
  }

  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;
}

Used to dispatch an action to change the current state. It’s also the only way to change the state. It accepts an action describing the operation as an argument and returns this action as the function’s return value.

From the preceding checks in the code, it’s clear that an action must be a plain object and must contain a type property.

if (isDispatching) {
  throw new Error("Reducers may not dispatch actions.");
}

From this, we can see that if the system is currently in the dispatch phase of a previous action, the current action might fail to dispatch.

Afterward, the current state is computed, and the listener functions in nextListeners are triggered in order.

replaceReducer#

/**
 * Replaces the reducer currently used by the store to calculate the state.
 *
 * You might need this if your app implements code splitting and you want to
 * load some of the reducers dynamically. You might also need this if you
 * implement a hot reloading mechanism for Redux.
 *
 * @param {Function} nextReducer The reducer for the store to use instead.
 * @returns {void}
 */
function replaceReducer(nextReducer) {
  if (typeof nextReducer !== "function") {
    throw new Error("Expected the nextReducer to be a function.");
  }

  currentReducer = nextReducer;
  dispatch({ type: ActionTypes.INIT });
}

Replaces the current reducer and dispatches an internal action for initialization.

export const ActionTypes = {
  INIT: "@@redux/INIT",
};

observable#

/**
 * Interoperability point for observable/reactive libraries.
 * @returns {observable} A minimal observable of state changes.
 * For more information, see the observable proposal:
 * https://github.com/tc39/proposalobservable
 */
function observable() {
  const outerSubscribe = subscribe;
  return {
    /**
     * The minimal observable subscription method.
     * @param {Object} observer Any object that can be used as an observer.
     * The observer object should have a `next` method.
     * @returns {subscription} An object with an `unsubscribe` method that can
     * be used to unsubscribe the observable from the store, and prevent further
     * emission of values from the observable.
     */
    subscribe(observer) {
      if (typeof observer !== "object") {
        throw new TypeError("Expected the observer to be an object.");
      }

      function observeState() {
        if (observer.next) {
          observer.next(getState());
        }
      }

      observeState();
      const unsubscribe = outerSubscribe(observeState);
      return { unsubscribe };
    },

    [$observable]() {
      return this;
    },
  };
}

A method used to make an object observable, generally not used in typical scenarios.

Finally#

// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
dispatch({ type: ActionTypes.INIT });

return {
  dispatch,
  subscribe,
  getState,
  replaceReducer,
  [$observable]: observable,
};

Dispatches an INIT action to initialize, prompting all reducers to return their default initial state.

Then, the above functions are returned as the API for the store created via createStore.

combineReducers#

This module is used to combine multiple reducers into a single reducer. Its dependent modules are:

  • ActionTypes
  • isPlainObject
  • warning

Let’s examine the contents of combineReducers one by one.

getUndefinedStateErrorMessage#

function getUndefinedStateErrorMessage(key, action) {
  const actionType = action && action.type;
  const actionName =
    (actionType && `"${actionType.toString()}"`) || "an action";

  return (
    `Given action ${actionName}, reducer "${key}" returned undefined. ` +
    `To ignore an action, you must explicitly return the previous state. ` +
    `If you want this reducer to hold no value, you can return null instead of undefined.`
  );
}

Defines a function to generate an error message when a reducer returns undefined. Nothing much to add here.

getUnexpectedStateShapeWarningMessage#

function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache,
) {
  const reducerKeys = Object.keys(reducers);
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? "preloadedState argument passed to createStore"
      : "previous state received by the reducer";

  if (reducerKeys.length === 0) {
    return (
      "Store does not have a valid reducer. Make sure the argument passed " +
      "to combineReducers is an object whose values are reducers."
    );
  }

  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/\s([az|AZ]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    );
  }

  const unexpectedKeys = Object.keys(inputState).filter(
    (key) => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key],
  );

  unexpectedKeys.forEach((key) => {
    unexpectedKeyCache[key] = true;
  });

  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? "keys" : "key"} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    );
  }
}

From the function name, ‘Get Unexpected State Shape Warning Message,’ it’s clear that this function generates an error message when the structure of the passed inputState is incorrect.

Reducers must have keys (which is obvious). inputState must be a plain object. All keys of inputState should be among the reducer’s own properties (not inherited from the prototype chain) and should not be in the unexpectedKeyCache.

assertReducerShape#

function assertReducerShape(reducers) {
  Object.keys(reducers).forEach((key) => {
    const reducer = reducers[key];
    const initialState = reducer(undefined, { type: ActionTypes.INIT });

    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.`,
      );
    }

    const type =
      "@@redux/PROBE_UNKNOWN_ACTION_" +
      Math.random().toString(36).substring(7).split("").join(".");
    if (typeof reducer(undefined, { type }) === "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.`,
      );
    }
  });
}

Used to ensure that the structure of the passed reducers is correct. This means each reducer must return a non-undefined initState after receiving an INIT action, and this action should not be specifically handled within the reducer. This is why we must specify a default return state within our reducers.

combineReducers#

/**
 * Turns an object whose values are different reducer functions, into a single
 * reducer function. It will call every child reducer, and gather their results
 * into a single state object, whose keys correspond to the keys of the passed
 * reducer functions.
 *
 * @param {Object} reducers An object whose values correspond to different
 * reducer functions that need to be combined into one. One handy way to obtain
 * it is to use ES6 `import * as reducers` syntax. The reducers may never return
 * undefined for any action. Instead, they should return their initial state
 * if the state passed to them was undefined, and the current state for any
 * unrecognized action.
 *
 * @returns {Function} A reducer function that invokes every reducer inside the
 * passed object, and builds a state object with the same shape.
 */
export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers);
  const finalReducers = {};
  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}"`);
      }
    }

    if (typeof reducers[key] === "function") {
      finalReducers[key] = reducers[key];
    }
  }
  const finalReducerKeys = Object.keys(finalReducers);

  let unexpectedKeyCache;
  if (process.env.NODE_ENV !== "production") {
    unexpectedKeyCache = {};
  }

  let shapeAssertionError;
  try {
    assertReducerShape(finalReducers);
  } catch (e) {
    shapeAssertionError = e;
  }

  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError;
    }

    if (process.env.NODE_ENV !== "production") {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache,
      );
      if (warningMessage) {
        warning(warningMessage);
      }
    }

    let hasChanged = false;
    const nextState = {};
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i];
      const reducer = finalReducers[key];
      const previousStateForKey = state[key];
      const nextStateForKey = reducer(previousStateForKey, action);
      if (typeof nextStateForKey === "undefined") {
        const errorMessage = getUndefinedStateErrorMessage(key, action);
        throw new Error(errorMessage);
      }
      nextState[key] = nextStateForKey;
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    return hasChanged ? nextState : state;
  };
}

combineReducers accepts an object of reducers to be combined into a single reducer. After execution, it returns a function, which is our rootReducer.

First, the passed reducers are iterated by key and assigned to finalReducers. Then, a series of error checks are performed, and finally, a function combination is returned, which is our combined reducer:

let hasChanged = false
const nextState = {}
// 遍历 finalReducerKeys
for (let i = 0; i < finalReducerKeys.length; i++) {
// 拿到当前的 reducer key
    const key = finalReducerKeys[i]
// 根据 reducer key 拿到具体的 reducer 函数
      const reducer = finalReducers[key]
// 获取之前的 key 对应的 state
      const previousStateForKey = state[key]
// 计算下一个当前 key 对应的 state
      const nextStateForKey = reducer(previousStateForKey, action)
// 如果计算出来的 state 为 undefined 那么报错
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
// 把当前 key 对应的 state 赋值到下一个全局 state
      nextState[key] = nextStateForKey
// 只要有一个 key 对应的 state 发生了变化,那么就认为整个 state 发生了变化
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 根据 state 是否发生变化,返回下一个 state 或者上一个 state
    return hasChanged ? nextState : state
  }

bindActionCreators#

This function is very simple; it’s a helper function. It’s used to bind dispatch to an actionCreator, so you can dispatch an action by directly calling the bound function, without needing dispatch(actionCreator(...)).

applyMiddleware#

This is a key section, and often difficult for beginners to grasp, so let’s examine it carefully.

import compose from "./compose";

/**
 * Creates a store enhancer that applies middleware to the dispatch method
 * of the Redux store. This is handy for a variety of tasks, such as expressing
 * asynchronous actions in a concise manner, or logging every action payload.
 *
 * See `reduxthunk` package as an example of the Redux middleware.
 *
 * Because middleware is potentially asynchronous, this should be the first
 * store enhancer in the composition chain.
 *
 * Note that each middleware will be given the `dispatch` and `getState` functions
 * as named arguments.
 *
 * @param {...Function} middlewares The middleware chain to be applied.
 * @returns {Function} A store enhancer applying the middleware.
 */
export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer);
    let dispatch = store.dispatch;
    let chain = [];

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action),
    };
    chain = middlewares.map((middleware) => middleware(middlewareAPI));
    dispatch = compose(...chain)(store.dispatch);

    return {
      ...store,
      dispatch,
    };
  };
}

The code is very short and depends on the compose module.

The applyMiddleware function accepts a series of middleware functions as arguments and returns a closure function that has access to createStore. This function, in turn, accepts reducer, preloadedState, and enhancer as arguments. Let’s look at it in conjunction with the createStore function:

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }

When we create the store like this:

const store = createStore(reducer, applyMiddleware(...middleware));

Since the second argument of createStore is a function, it will proceed to:

return enhancer(createStore)(reducer, preloadedState);

This means the result of applyMiddleware(...middleware) takes over createStore. The actual store is created by calling createStore again inside applyMiddleware, at which point preloadedState and enhancer are both undefined.

// applyMiddleware
const store = createStore(reducer, preloadedState, enhancer);

Let’s go back and continue.

//applyMiddleware
dispatch = compose(...chain)(store.dispatch);

Here, we first need to look at the compose module. Its purpose is to achieve something like compose(f, g, h) > (...args) => f(g(h(...args))).

So, the dispatch here is the dispatch that has been enhanced and wrapped by middleware on top of store.dispatch.

const middlewareAPI = {
  getState: store.getState,
  dispatch: (action) => dispatch(action),
};
// 把 middlewareAPI 传入到每个中间件中
chain = middlewares.map((middleware) => middleware(middlewareAPI));

The dispatch: (action) => dispatch(action) here indicates that the dispatch within each middleware is independent and doesn’t affect others, preventing any single middleware from altering the behavior of dispatch for others. Then, getState and dispatch are passed as arguments to each middleware.

return {
  ...store,
  dispatch,
};

Finally, the enhanced dispatch overwrites the original dispatch in the store.

Looking at the entire middleware code, it might seem a bit abstract, so let’s look at an example:

errorMiddleware

export default ({ dispatch, getState }) =>
  (next) =>
  (action) => {
    const { error, payload } = action;
    if (error) {
      dispatch(showToast(payload.message || payload.toString()));
    }
    return next(action);
  };

This is our error handling middleware. It’s also a higher-order function that first accepts dispatch and getState as parameters, then returns a function that accepts next as a parameter. dispatch and getState are passed into the middleware via middlewareAPI in the code above.

Then, let’s continue to look at the function returned by errorMiddleware that accepts next as a parameter. next is actually the next middleware to be executed.

Next, we need to understand the execution order of middleware. To describe the propagation of an action through middleware more clearly, let’s assume we have the following three middleware functions:

const mid1 = () => (next) => (action) => {
  console.log("mid1 before");
  next(action);
  console.log("mid1 after");
};
const mid2 = () => (next) => (action) => {
  console.log("mid2 before");
  next(action);
  console.log("mid2 after");
};
const mid3 = () => (next) => (action) => {
  console.log("mid3 before");
  next(action);
  console.log("mid3 after");
};

After executing applyMiddleware(mid1, mid2, mid3), and passing through the following code:

dispatch = compose(...chain)(store.dispatch);

We get:

dispatch = (store.dispatch) => mid1(mid2(mid3(store.dispatch)))

Here, each midx is the result of executing middleware(middlewareAPI). So, the next value for mid3 is store.dispatch. The next for mid2 is mid3(store.dispatch), and so on. The next for mid1 is mid2(mid3(store.dispatch)). This is why calling next within a middleware allows the action to proceed to the next middleware.

When we dispatch an action, the order printed to the console is as follows:

mid1 before
mid2 before
mid3 before
mid3 after
mid2 after
mid1 after

We can see the flow is as follows:

  1. Execute the code in mid1 before the next method call.
  2. Execute the code in mid2 before the next method call.
  3. Execute the code in mid3 before the next method call.
  4. Execute dispatch to dispatch the action.
  5. Execute the code in mid3 after the next method call.
  6. Execute the code in mid2 after the next method call.
  7. Execute the code in mid1 after the next method call.

A diagram will make it clearer:

reduxmiddleware

The red path represents the flow we just described. You can also see a black path, which illustrates what happens if we directly call dispatch within mid2. Let’s modify mid2:

const mid2 =
  ({ dispatch, getStore }) =>
  (next) =>
  (action) => {
    console.log("mid2 before");
    dispatch(action);
    console.log("mid2 after");
  };

If we change it like this, what do you think will happen?

The answer is, it will enter an infinite loop between ‘mid1 before’ and ‘mid2 before’. This is because calling dispatch will cause the action to re-traverse all middleware, as shown by the black path in the diagram. Therefore, when we need to call dispatch within a middleware, we must add a condition to the action, only calling dispatch when that condition is met to avoid an infinite loop. Let’s refactor mid2:

const mid2 = ({ dispatch, getStore }) => next => action => {
  console.log('mid2 before')
  if(action.isApi) {
    dispatch({
      isApi: false,
      ...
    })
  }
  dispatch(action)
  console.log('mid2 after')
}

This way, an action that does not satisfy the isApi condition will only be dispatched when the action does satisfy the isApi condition, thus preventing an infinite loop. This method is often used when dispatching asynchronous actions. For example, our callAPIMiddleware used for data requests in a production environment:

export default ({dispatch, getState}) => {
return next => action => {
  const {
    types,
    api,
    callType,
    meta,
    body,
    shouldCallAPI
  } = action
  const state = getState()
  const callTypeList = ['get', 'post']
  if (!api) {
    return next(action)
  }
  if (!(types.start && types.success && types.failure)) {
    throw new Error('Expected types has start && success && failure keys.')
  }
  if (callTypeList.indexOf(callType) === 1) {
    throw new Error(`API callType Must be one of ${callTypeList}`)
  }

  const {start, success, failure} = types
  if (!shouldCallAPI(state)) {
    return false
  }

  dispatch({
    type: start,
    payload: {
      ...body
    },
    meta
  })
  const mapCallTypeToFetch = {
      post: () => fetch(api, {
        method: 'post',
        // credentials 设置为每次请求都带上 cookie
        credentials: 'include',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(bodyWithSource)
      }),
      get: () => {
        const toString = Object.keys(bodyWithSource).map(function (key, index) {
          return encodeURIComponent(key) + '=' + encodeURIComponent(bodyWithSource[key])
        }).join('&')
        return fetch(`${api}?${toString}`, {
          method: 'get',
          credentials: 'include',
          headers: {
            'Accept': 'application/json'
          }
        })
      }
    }
    const fetching = mapCallTypeToFetch[callType]()
... 省略一堆业务逻辑
  return fetching.then(res => {
    clearTimeout(loadingTimer)
    dispatch(hideLoading())
    if (res.ok) {
      try {
        return res.json()
      } catch (err) {
        throw new Error(err)
      }
    } else {
      dispatch(showToast('请求出错'))
      return Promise.reject(res.text())
    }
  })
    .then(res => resBehaviour(res))
    .then(res => {
      dispatch({
        type: success,
        meta,
        payload: {
          ...res.data
        }
      })
      return Promise.resolve(res)
    })
    .catch(err => {
      console.error(`接口请求出错,${err}`)
      return Promise.reject(err)
    })
}

That’s all for middleware; hopefully, everyone can understand it now.

Summary#

Overall, Redux’s source code is very concise, yet its various implementations are remarkably ingenious.

Furthermore, the author places great emphasis on developer experience; the comments are very detailed, making the code relatively easy to read. Error handling is also very thorough, helping developers pinpoint issues more easily.

Finally, due to my limited ability, if there are any errors in the article, please point them out so we can discuss them together.

This article was published on August 18, 2017 and last updated on August 18, 2017, 2971 days ago. The content may be outdated.

Redux Source Code Deep Dive (Long Read)
https://blog.kisnows.com/en-US/2017/08/18/redux-source-code-read/
Author
Kisnows
Published at
2017-08-18
License
CC BY-NC-ND 4.0