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:
- Execute the code in
mid1
before thenext
method call. - Execute the code in
mid2
before thenext
method call. - Execute the code in
mid3
before thenext
method call. - Execute
dispatch
to dispatch the action. - Execute the code in
mid3
after thenext
method call. - Execute the code in
mid2
after thenext
method call. - Execute the code in
mid1
after thenext
method call.
A diagram will make it clearer:
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.