Redux 源码解读 —— 从源码开始学 Redux
已经快一年没有碰过 React 全家桶了,最近换了个项目组要用到 React 技术栈,所以最近又复习了一下;捡起旧知识的同时又有了一些新的收获,在这里作文以记之。
在阅读文章之前,最好已经知道如何使用 Redux(不是 React-Redux)。
一、准备环境
为了更好的解读源码,我们可以把源码拷贝到本地,然后搭建一个开发环境。Redux 的使用不依赖于 React,所以你完全可以在一个极为简单的 JavaScript 项目中使用它。这里不再赘述开发环境的搭建过程,需要的同学可以直接拷贝我的代码到本地,然后安装依赖,运行项目。
$ git clone https://github.com/zhongdeming428/redux && cd redux
$ npm i
$ npm run dev
二、阅读源码
(1)源代码结构
忽略项目中的那些说明文档什么的,只看 src 这个源文件目录,其结构如下:
src
├── applyMiddleware.js // 应用中间件的 API
├── bindActionCreators.js // 转换 actionCreators 的 API
├── combineReducers.js // 组合转换 reducer 的 API
├── compose.js // 工具函数,用于嵌套调用中间件
├── createStore.js // 入口函数,创建 store 的 API
├── index.js // redux 项目的入口文件,用于统一暴露所有 API
├── test
│ └── index.js // 我所创建的用于调试的脚本
└── utils // 专门放工具函数的目录
├── actionTypes.js // 定义了一些 redux 预留的 action type
├── isPlainObject.js // 用于判断是否是纯对象
└── warning.js // 用于抛出合适的警告信息
可以看出来 redux 的源码结构简单清晰明了,几个主要的(也是仅有的) API 被尽可能的分散到了单个的文件模块中,我们只需要挨个的看就行了。
(2)index.js
上一小节说到 index.js 是 redux 项目的入口文件,用于暴露所有的 API,所以我们来看看代码:
import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'
// 不同的 API 写在不同的 js 文件中,最后通过 index.js 统一导出。
// 这个函数用于判断当前代码是否已经被打包工具(比如 Webpack)压缩过,如果被压缩过的话,
// isCrushed 函数的名称会被替换掉。如果被替换了函数名但是 process.env.NODE_ENV 又不等于 production
// 的时候,提醒用户使用生产环境下的精简代码。
function 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 loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
'or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' +
'to ensure you have the correct code for your production build.'
)
}
// 导出主要的 API。
export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose,
__DO_NOT_USE__ActionTypes
}
我删除了所有的英文注释以减小篇幅,如果大家想看原来的注释,可以去 redux 的项目查看源码。
可以看到在程序的头部引入了所有的 API 模块以及工具函数,然后在底部统一导出了。这一部分比较简单,主要是 isCrushed 函数有点意思。作者用这个函数来判断代码是否被压缩过(判断函数名是否被替换掉了)。
这一部分也引用到了工具函数,由于这几个函数比较简单,所以可以先看看它们是干嘛的。
(3)工具函数
除了 compose 函数以外,所有的工具函数都被放在了 utils 目录下。
actionTypes.js
这个工具模块定义了几种 redux 预留的 action type,包括 reducer 替换类型、reducer 初始化类型和随机类型。下面上源码:
// 定义了一些 redux 保留的 action type。
// 随机字符串确保唯一性。
const randomString = () =>
Math.random()
.toString(36)
.substring(7)
.split('')
.join('.')
const ActionTypes = {
INIT: `@@redux/INIT${randomString()}`,
REPLACE: `@@redux/REPLACE${randomString()}`,
PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}
export default ActionTypes
可以看出就是返回了一个 ActionTypes 对象,里面包含三种类型:INIT、REPLACE 和 PROBE_UNKNOW_ACTION。分别对应之前所说的几种类型,为了防止和用户自定义的 action type 相冲突,刻意在 type 里面加入了随机值。在后面的使用中,通过引入 ActionType 对象来进行对比。
isPlainObject.js
这个函数用于判断传入的对象是否是纯对象,因为 redux 要求 action 和 state 是一个纯对象,所以这个函数诞生了。
上源码:
/**
* 判断一个参数是否是纯对象,纯对象的定义就是它的构造函数为 Object。
* 比如: { name: 'isPlainObject', type: 'funciton' }。
* 而 isPlainObject 这个函数就不是纯对象,因为它的构造函数是 Function。
* @param {any} obj 要检查的对象。
* @returns {boolean} 返回的检查结果,true 代表是纯对象。
*/
export default function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false
let proto = obj
// 获取最顶级的原型,如果就是自身,那么说明是纯对象。
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto
}
warning.js
这个函数用于抛出适当的警告,没啥好说的。
/**
* Prints a warning in the console if it exists.
*
* @param {String} message The warning message.
* @returns {void}
*/
export default function warning(message) {
/* eslint-disable no-console */
if (typeof console !== 'undefined' && typeof console.error === 'function') {
console.error(message)
}
/* eslint-enable no-console */
try {
// This error was thrown as a convenience so that if you enable
// "break on all exceptions" in your console,
// it would pause the execution at this line.
throw new Error(message)
} catch (e) {} // eslint-disable-line no-empty
}
compose.js
这个函数用于嵌套调用中间件(middleware),进行初始化。
/**
* 传入一系列的单参数函数作为参数(funcs 数组),返回一个新的函数,这个函数可以接受
* 多个参数,运行时会将 funcs 数组中的函数从右至左进行调用。
* @param {...Function} funcs 一系列中间件。
* @returns {Function} 返回的结果函数。
* 从右至左调用,比如: compose(f, g, h) 将会返回一个新函数
* (...args) => f(g(h(...args))).
*/
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
// 通过 reduce 方法迭代。
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
(4)createStore.js
看完了工具函数和入口函数,接下来就要正式步入主题了。我们使用 redux 的重要一步就是通过 createStore 方法创建 store。那么接下来看看这个方法是怎么创建 store 的,store 又是个什么东西呢?
我们看源码:
import $$observable from 'symbol-observable'
// 后面会讲。
import ActionTypes from './utils/actionTypes'
// 引入一些预定义的保留的 action type。
import isPlainObject from './utils/isPlainObject'
// 判断一个对象是否是纯对象。
// 使用 redux 最主要的 API,就是这个 createStore,它用于创建一个 redux store,为你提供状态管理。
// 它接受三个参数(第二三个可选),第一个是 reducer,用于改变 redux store 的状态;第二个是初始化的 store,
// 即最开始时候 store 的快照;第三个参数是由 applyMiddleware 函数返回的 enhancer 对象,使用中间件必须
// 提供的参数。
export default function createStore(reducer, preloadedState, enhancer) {
// 下面这一段基本可以不看,它们是对参数进行适配的。
/*************************************参数适配****************************************/
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
// 如果传递了多个 enhancer,抛出错误。
throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function'
)
}
// 如果没有传递默认的 state(preloadedState 为函数类型,enhancer 为未定义类型),那么传递的
// preloadedState 即为 enhancer。
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
// 如果 enhancer 为不为空且非函数类型,报错。
throw new Error('Expected the enhancer to be a function.')
}
// 使用 enhancer 对 createStore 进行处理,引入中间件。注意此处没有再传递 enhancer 作为参数。实际上 enhancer 会对 createStore 进行处理,然后返回一个实际意义上的 createStore 用于创建 store 对象,参考 applyMiddleware.js。
return enhancer(createStore)(reducer, preloadedState)
}
// 如果 reducer 不是函数类型,报错。
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
/*********************************************************************************/
// 在函数内部定义一系列局部变量,用于存储数据。
let currentReducer = reducer // 存储当前的 reducer。
let currentState = preloadedState // 用于存储当前的 store,即为 state。
let currentListeners = [] // 用于存储通过 store.subscribe 注册的当前的所有订阅者。
let nextListeners = currentListeners // 新的 listeners 数组,确保不直接修改 listeners。
let isDispatching = false // 当前状态,防止 reducer 嵌套调用。
// 顾名思义,确保 nextListeners 可以被修改,当 nextListeners 与 currentListeners 指向同一个数组的时候
// 让 nextListeners 成为 currentListeners 的副本。防止修改 nextListeners 导致 currentListeners 发生变化。
// 一开始我也不是很明白为什么会存在 nextListeners,因为后面 dispatch 函数中还是直接把 nextListeners 赋值给了 currentListeners。
// 直接使用 currentListeners 也是可以的。后来去 redux 的 repo 搜了搜,发现了一个 issue(https://github.com/reduxjs/redux/issues/2157) 讲述了这个做法的理由。
// 提交这段代码的作者的解释(https://github.com/reduxjs/redux/commit/c031c0a8d900e0e95a4915ecc0f96c6fe2d6e92b)是防止 Array.slice 的滥用,只有在必要的时候调用 Array.slice 方法来复制 listeners。
// 以前的做法是每次 dispatch 都要 slice 一次,导致了性能的降低吧。
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
// 返回 currentState,即 store 的快照。
function getState() {
// 防止在 reducer 中调用该方法,reducer 会接受 state 参数。
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
}
// store.subscribe 函数,订阅 dispatch。
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
// 不允许在 reducer 中进行订阅。
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-reference/store#subscribe(listener) for more details.'
)
}
let isSubscribed = true
// 每次操作 nextListeners 之前先确保可以修改。
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-reference/store#subscribe(listener) for more details.'
)
}
isSubscribed = false
// 每次操作 nextListeners 之前先确保可以修改。
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
// store.dispatch 函数,用于触发 reducer 修改 state。
function dispatch(action) {
if (!isPlainObject(action)) {
// action 必须是纯对象。
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
// 每个 action 必须包含一个 type 属性,指定修改的类型。
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
// reducer 内部不允许派发 action。
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
// 调用 reducer 之前,先将标志位置一。
isDispatching = true
// 调用 reducer,返回的值即为最新的 state。
currentState = currentReducer(currentState, action)
} finally {
// 调用完之后将标志位置 0,表示 dispatch 结束。
isDispatching = false
}
// dispatch 结束之后,执行所有订阅者的函数。
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
// 返回当前所使用的 action,这一步是中间件嵌套使用的关键,很重要。
return action
}
// 一个比较新的 API,用于动态替换当前的 reducers。适用于按需加载,代码拆分等场景。
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
// 执行默认的 REPLACE 类型的 action。在 combineReducers 函数中有使用到这个类型。
currentReducer = nextReducer
dispatch({ type: ActionTypes.REPLACE })
}
// 这是为了适配 ECMA TC39 会议的一个有关 Observable 的提案(参考https://github.com/tc39/proposal-observable)所写的一个函数。
// 作用是订阅 store 的变化,适用于所有实现了 Observable 的类库(主要是适配 RxJS)。
// 我找到了引入这个功能的那个 commit:https://github.com/reduxjs/redux/pull/1632。
function observable() {
// outerSubscribe 即为外部的 subscribe 函数。
const outerSubscribe = subscribe
// 返回一个纯对象,包含 subscribe 方法。
return {
subscribe(observer) {
if (typeof observer !== 'object' || observer === null) {
throw new TypeError('Expected the observer to be an object.')
}
// 用于给 subscribe 注册的函数,严格按照 Observable 的规范实现,observer 必须有一个 next 属性。
function observeState() {
if (observer.next) {
observer.next(getState())
}
}
observeState()
const unsubscribe = outerSubscribe(observeState)
return { unsubscribe }
},
// $$observable 即为 Symbol.observable,也属于 Observable 的规范,返回自身。
[$$observable]() {
return this
}
}
}
// 初始化时 dispatch 一个 INIT 类型的 action,校验各种情况。
dispatch({ type: ActionTypes.INIT })
// 返回一个 store 对象。
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}
不难发现,我们的 store 对象就是一个纯 JavaScript 对象。包含几个属性 API,而我们的 state 就存储在 createStore 这个方法内部,是一个局部变量,只能通过 getState 方法访问到。这里实际上是对闭包的利用,所有我们操作的 state 都存储在 getState 方法内部的一个变量里面。直到我们的程序结束(或者说 store 被销毁),createStore 方法才会被回收,里面的变量才会被销毁。
而 subscribe 方法就是对观察者模式的利用(注意不是发布订阅模式,二者有区别,不要混淆),我们通过 subscribe 方法注册我们的函数,我们的函数会给存储到 createStore 方法的一个局部变量当中,每次 dispatch 被调用之后,都会遍历一遍 currentListeners,依次执行其中的方法,达到我们订阅的要求。
(5)combineReducers.js
了解了 createStore 到底是怎么一回事,我们再来看看 combineReducers 到底是怎么创建 reducer 的。
我们写 reducer 的时候,实际上是在写一系列函数,然后整个到一个对象的属性上,最后传给 combineReducers 进行处理,处理之后就可以供 createStore 使用了。
例如:
// 创建我们的 reducers。
const _reducers = {
items(items = [], { type, payload }) {
if (type === 'ADD_ITEMS') items.push(payload);
return items;
},
isLoading(isLoading = false, { type, payload }) {
if (type === 'IS_LOADING') return true;
return false;
}
};
// 交给 combineReducers 处理,适配 createStore。
const reducers = combineReducers(_reducers);
// createStore 接受 reducers,创建我们需要的 store。
const store = createStore(reducers);
那么 combineReducers 对我们的 reducers 对象进行了哪些处理呢?
下面的代码比较长,希望大家能有耐心。
import ActionTypes from './utils/actionTypes'
import warning from './utils/warning'
import isPlainObject from './utils/isPlainObject'
/**
* 用于获取错误信息的工具函数,如果调用你所定义的某个 reducer 返回了 undefined,那么就调用这个函数
* 抛出合适的错误信息。
*
* @param {String} key 你所定义的某个 reducer 的函数名,同时也是 state 的一个属性名。
*
* @param {Object} action 调用 reducer 时所使用的 action。
*/
function getUndefinedStateErrorMessage(key, action) {
const actionType = action && action.type
const actionDescription =
(actionType && `action "${String(actionType)}"`) || 'an action'
return (
`Given ${actionDescription}, 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.`
)
}
/**
* 工具函数,用于校验未知键,如果 state 中的某个属性没有对应的 reducer,那么返回报错信息。
* 对于 REPLACE 类型的 action type,则不进行校验。
* @param {Object} inputState
* @param {Object} reducers
* @param {Object} action
* @param {Object} unexpectedKeyCache
*/
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'
// 如果 reducers 长度为 0,返回对应错误信息。
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([a-z|A-Z]+)/)[1] + // {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1]
`". Expected argument to be an object with the following ` + // 返回的是 inputState 的类型。
`keys: "${reducerKeys.join('", "')}"`
)
}
// 获取所有 State 有而 reducers 没有的属性,加入到 unexpectedKeysCache。
const unexpectedKeys = Object.keys(inputState).filter(
key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
)
// 加入到 unexpectedKeyCache。
unexpectedKeys.forEach(key => {
unexpectedKeyCache[key] = true
})
// 如果是 REPLACE 类型的 action type,不再校验未知键,因为按需加载的 reducers 不需要校验未知键,现在不存在的 reducers 可能下次就加上了。
if (action && action.type === ActionTypes.REPLACE) return
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.`
)
}
}
/**
* 用于校验所有 reducer 的合理性:传入任意值都不能返回 undefined。
* @param {Object} reducers 你所定义的 reducers 对象。
*/
function assertReducerShape(reducers) {
Object.keys(reducers).forEach(key => {
const reducer = reducers[key]
// 获取初始化时的 state。
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.`
)
}
// 如果初始化校验通过了,有可能是你定义了 ActionTypes.INIT 的操作。这时候重新用随机值校验。
// 如果返回 undefined,说明用户可能对 INIT type 做了对应处理,这是不允许的。
if (
typeof reducer(undefined, {
type: ActionTypes.PROBE_UNKNOWN_ACTION()
}) === '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.`
)
}
})
}
// 把你所定义的 reducers 对象转化为一个庞大的汇总函数。
// 可以看出,combineReducers 接受一个 reducers 对象作为参数,
// 然后返回一个总的函数,作为最终的合法的 reducer,这个 reducer
// 接受 action 作为参数,根据 action 的类型遍历调用所有的 reducer。
export default function combineReducers(reducers) {
// 获取 reducers 所有的属性名。
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
// 遍历 reducers 的所有属性,剔除所有不合法的 reducer。
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') {
// 将 reducers 中的所有 reducer 拷贝到新的 finalReducers 对象上。
finalReducers[key] = reducers[key]
}
}
// finalReducers 是一个纯净的经过过滤的 reducers 了,重新获取所有属性名。
const finalReducerKeys = Object.keys(finalReducers)
let unexpectedKeyCache
// unexpectedKeyCache 包含所有 state 中有但是 reducers 中没有的属性。
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
let shapeAssertionError
try {
// 校验所有 reducer 的合理性,缓存错误。
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
// 这就是返回的新的 reducer,一个纯函数。每次 dispatch 一个 action,都要执行一遍 combination 函数,
// 进而把你所定义的所有 reducer 都执行一遍。
return function combination(state = {}, action) {
if (shapeAssertionError) {
// 如果有缓存的错误,抛出。
throw shapeAssertionError
}
if (process.env.NODE_ENV !== 'production') {
// 非生产环境下校验 state 中的属性是否都有对应的 reducer。
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
let hasChanged = false
// state 是否改变的标志位。
const nextState = {}
// reducer 返回的新 state。
for (let i = 0; i < finalReducerKeys.length; i++) { // 遍历所有的 reducer。
const key = finalReducerKeys[i] // 获取 reducer 名称。
const reducer = finalReducers[key] // 获取 reducer。
const previousStateForKey = state[key] // 旧的 state 值。
const nextStateForKey = reducer(previousStateForKey, action) // 执行 reducer 返回的新的 state[key] 值。
if (typeof nextStateForKey === 'undefined') {
// 如果经过了那么多校验,你的 reducer 还是返回了 undefined,那么就要抛出错误信息了。
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
// 把返回的新值添加到 nextState 对象上,这里可以看出来,你所定义的 reducer 的名称就是对应的 state 的属性,所以 reducer 命名要规范!
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
// 检验 state 是否发生了变化。
}
// 根据标志位返回对应的 state。
return hasChanged ? nextState : state
}
}
(6)applyMiddleware.js
combineReducers 方法代码比较多,但是实际逻辑还是很简单的,接下来这个函数代码不多,但是逻辑要稍微复杂一点,它就是应用中间件的 applyMiddleware 函数。这个函数的思想比较巧妙,值得学习。
import compose from './compose'
// 用于应用中间件的函数,可以同时传递多个中间件。中间件的标准形式为:
// const middleware = store => next => action => { /*.....*/ return next(action); }
export default function applyMiddleware(...middlewares) {
// 返回一个函数,接受 createStore 作为参数。args 参数即为 reducer 和 preloadedState。
return createStore => (...args) => {
// 在函数内部调用 createStore 创建一个 store 对象,这里不会传递 enhancer,因为 applyMiddleware 本身就是在创建一个 enhancer,然后给 createStore 调用。
// 这里实际上是通过 applyMiddleware 把 store 的创建推迟了。为什么要推迟呢?因为要利用 middleWares 做文章,先初始化中间件,重新定义 dispatch,然后再创建 store,这时候创建的 store 所包含的 dispatch 方法就区别于不传递 enhancer 时所创建的 dispatch 方法了,其中包含了中间件所定义的一些逻辑,这就是为什么中间件可以干预 dispatch 的原因。
const store = createStore(...args)
// 这里对 dispatch 进行了重新定义,不管传入什么参数,都会报错,这样做的目的是防止你的中间件在初始化的时候就
// 调用 dispatch。
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: (...args) => dispatch(...args) // 注意最后 dispatch 的时候不会访问上面报错的那个 dispatch 函数了,因为那个函数被下面的 dispatch 覆盖了。
}
// 对于每一个 middleware,都传入 middlewareAPI 进行调用,这就是中间件的初始化。
// 初始化后的中间件返回一个新的函数,这个函数接受 store.dispatch 作为参数,返回一个替换后的 dispatch,作为新的
// store.dispatch。
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// compose 方法把所有中间件串联起来调用。用最终结果替换 dispatch 函数,之后所使用的所有 store.dispatch 方法都已经是
// 替换了的,加入了新的逻辑。
dispatch = compose(...chain)(store.dispatch)
// 初始化中间件以后,把报错的 dispatch 函数覆盖掉。
/**
* middle 的标准形式:
* const middleware = ({ getState, dispatch }) => next => action => {
* // ....
* return next(action);
* }
* 这里 next 是经过上一个 middleware 处理了的 dispatch 方法。
* next(action) 返回的仍然是一个 dispatch 方法。
*/
return {
...store,
dispatch // 全新的 dispatch。
}
}
}
代码量真的很少,但是真的很巧妙,这里有几点很关键:
-
compose 方法利用
Array.prototype.reduce
实现中间件的嵌套调用,返回一个全新的函数,可以接受新的参数(上一个中间件处理过的 dispatch),最终返回一个全新的包含新逻辑的 dispatch 方法。你看 middleware 经过初始化后返回的函数的格式:next => action => { return next(action); }
其中
next
可以看成dispatch
,这不就是接受一个 dispatch 作为参数,然后返回一个新的 dispatch 方法吗?原因就是我们可以认为接受 action 作为参数,然后触发 reducer 更改 state 的所有函数都是 dispatch 函数。 -
middleware 中间件经过初始化以后,返回一个新函数,它接受 dispatch 作为参数,然后返回一个新的 dispatch 又可以供下一个 middleware 调用,这就导致所有的 middleware 可以嵌套调用了!而且最终返回的结果也是一个 dispatch 函数。
最终得到的 dispatch 方法,是把原始的 store.dispatch 方法传递给最后一个 middleware,然后层层嵌套处理,最后经过第一个 middleware 处理过以后所返回的方法。所以我们在调用应用了中间件的 dispatch 函数时,从左至右的经过了 applyMiddleware 方法的所有参数(middleware)的处理。这有点像是包裹和拆包裹的过程。
-
为什么 action 可以经过所有的中间件处理呢?我们再来看看中间件的基本结构:
({ dispatch, getState }) => next => action => { return next(action); }
我们可以看到 action 进入函数以后,会经过 next 的处理,并且会返回结果。next 会返回什么呢?因为第一个 next 的值就是 store.dispatch,所以看看 store.dispatch 的源码就知道了。
function dispatch(action) { // 省略了一系列操作的代码…… // 返回当前所使用的 action,这一步是中间件嵌套使用的关键。 return action }
没错,store.dispatch 最终返回了 action,由于中间件嵌套调用,所以每个 next 都返回 action,然后又可以供下一个 next 使用,环环相扣,十分巧妙。
这部分描述的有点拗口,语言捉急但又不想画图,各位还是自己多想想好了。
(7)bindActionCreators.js
这个方法没有太多好说的,主要作用是减少大家 dispatch reducer 所要写的代码,比如你原来有一个 action:
const addItems = item => ({
type: 'ADD_ITEMS',
payload: item
});
然后你要调用它的时候:
store.dispatch(addItems('item value'));
如果你使用 bindActionCreators:
const _addItems = bindActionCreators(addItems, store.dispatch);
当你要 dispatch reducer 的时候:
_addItems('item value');
这样就减少了你要写的重复代码,另外你还可以把所有的 action 写在一个对象里面传递给 bindActionCreators,就像传递给 combineReducers 的对象那样。
下面看看源码:
/**
* 该函数返回一个新的函数,调用新的函数会直接 dispatch ActionCreator 所返回的 action。
* 这个函数是 bindActionCreators 函数的基础,在 bindActionCreators 函数中会把 actionCreators 拆分成一个一个
* 的 ActionCreator,然后调用 bindActionCreator 方法。
* @param {Function} actionCreator 一个返回 action 纯对象的函数。
* @param {Function} dispatch store.dispatch 方法,用于触发 reducer。
*/
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
// 接受一个 actionCreator(或者一个 actionCreators 对象)和一个 dispatch 函数作为参数,
// 然后返回一个函数或者一个对象,直接执行这个函数或对象中的函数可以让你不必再调用 dispatch。
export default function bindActionCreators(actionCreators, dispatch) {
// 如果 actionCreators 是一个函数而非对象,那么直接调用 bindActionCreators 方法进行转换,此时返回
// 结果也是一个函数,执行这个函数会直接 dispatch 对应的 action。
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
// actionCreators 既不是函数也不是对象,或者为空时,抛出错误。
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error(
`bindActionCreators expected an object or a function, instead received ${
actionCreators === null ? 'null' : typeof actionCreators
}. ` +
`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
)
}
// 如果 actionCreators 是一个对象,那么它的每一个属性就应该是一个 actionCreator,遍历每一个 actionCreator,
// 使用 bindActionCreator 进行转换。
const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
// 把转换结果绑定到 boundActionCreators 对象,最后会返回它。
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
这部分挺简单的,主要作用在于把 action creator 转化为可以直接使用的函数。
三、中间件
看了源码以后,觉得中间件并没有想象中的那么晦涩难懂了。就是一个基本的格式,然后你在你的中间件里面可以为所欲为,最后调用固定的方法,返回固定的内容就完事了。这就是为什么大多数 redux middleware 的源码都很短小精悍的原因。
看看 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;
是不是很短很小?那么到底干了什么让它这么受欢迎呢?
实际上 redux-thunk 可以被认为就是:
// 这就是典型的 middleware 格式。
({ dispatch, getState }) => next => action => {
// next 就是 dispatch 方法。注释所在的函数就是返回的新的 dispatch。
// 先判断一下 action 是不是一个函数。
if (typeof action === 'function') {
// 如果是函数,调用它,传递 dispatch,getState 和 多余的参数作为 aciton 的参数。
return action(dispatch, getState, extraArgument);
}
// 如果 action 不是函数,直接 nextr调用 action,返回结果就完事儿了。
return next(action);
};
怎么样,是不是很简单?它干的事就是判断了一下 action 的类型,如果是函数就调用,不是函数就用 dispatch 来调用,很简单。
但是它实现的功能很实用,允许我们传递函数作为 store.dispatch 的参数,这个函数的参数应该是固定的,必须符合上面代码的要求,接受 dispatch、getState作为参数,然后这个函数应该返回实际的 action。
我们也可以写一个自己的中间件了:
({ dispatch, getState }) => next => action => {
return action.then ? action.then(next) : next(action);
}
这个中间件允许我们传递一个 Promise 对象作为 action,然后会等 action 返回结果(一个真正的 action)之后,再进行 dispatch。
当然由于 action.then() 返回的不是实际上的 action(一个纯对象),所以这个中间件可能没法跟其他中间件一起使用,不然其他中间件接受不到 action 会出问题。这只是个示例,用于说明中间件没那么复杂,但是我们可以利用中间件做很多事情。
如果想要了解更加复杂的 redux 中间件,可以参考:
四、总结
- Redux 精妙小巧,主要利用了闭包和观察者模式,然后运用了职责链、适配器等模式构建了一个 store 王国。store 拥有自己的领土,要想获取或改变 store 里面的内容,必须通过 store 的各个函数来实现。
- Redux 相比于 Vuex 而言,代码量更小,定制化程度更低,这就导致易用性低于 Vuex,但是可定制性高于 Vuex。这也符合 Vue 和 React 的风格。
- Redux 源码比较好懂,读懂源码更易于掌握 Redux 的使用方法,不要被吓倒。
- Redux 中间件短小精悍,比较实用。如果从使用方法开始学中间件比较难懂的话,可以尝试从源码学习中间件。
最后,时间紧迫,水平有限,难免存在纰漏或错误,请大家多多包涵、多多指教、共同进步。