Redux源码解析

2018年4月22日更新

GitHub上的redux源码已经更新几个小版本,虽然有一些细节上的区别,但是核心代码逻辑还是大同小异。

概述

本文较长,希望能对redux实现感兴趣的同学有一定的帮助。

redux是React生态系统中非常重要的一个存在,它是React最流行的状态管理库。redux的源码并不多,如果你打开redux的源码,你会发现它其实只有7个文件,加上注释一共也只有500多行代码,所以就算阅读它全部的源码也不会太费工夫。

基本结构

redux是开源的,我们可以在GitHub(参考1)上找到所有的源码。

1
2
3
4
5
6
7
8
9
── src
  ├── applyMiddleware.js
   ├── bindActionCreators.js
   ├── combineReducers.js
   ├── compose.js
   ├── createStore.js
   ├── index.js
   └── utils
   └── warning.js

这就是redux的全部源码,可以看到,文件名大都是我们熟悉的API。

入口文件index.js我们不分析,实际上它就是检查了一下当前执行的环境(开发环境还是生产环境),然后将其他各个文件引入再导出为一个整体的对象。

createStore

当我们使用redux时,一般是在写完actionTypes,actions,reducers,然后就从调用createStore开始创建一个store,所以我们就先拿它开刀。

createStore.js是这几个文件中代码量最多的一个(当前版本共255行),但是逻辑却是非常简单。

createStore本身是一个大的闭包,函数接收3个参数。

1
2
3
4
// ...
export default function createStore(reducer, preloadedState, enhancer) {
// ...
}

reducer我们都很熟悉,redux依据reducer来构建和修改应用的状态,preloadedState用于指定一个默认的状态,注意如果我们使用了combineReducers这个API,则默认提供的preloadedState的键也要和combineReducers中的键一一对应,否则会报错,enhancer则而已用于扩展和修改redux的默认功能,最常见的例子就是applyMiddleware了。

其实createStore在创建store的时候会dispatch一个默认的action,该action也会触发reducer,reducer会返回指定的默认状态,store就会将其当做初始状态,所以第2个参数preloadedState好像并没有什么用(不确定是不是我理解错了),所以一般情况下建议,在reducer中声明默认返回值当做初始状态。

函数一开始对参数做了类型检查,并且如果参数有缺失,则对参数顺序进行调整,然后定义了几个内部变量。

1
2
3
4
5
6
7
// ...
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
// ...

上面的reducer,就是传进到createStore方法中的reducer,reducer使redux修改状态树的关键依据,它被保存在了currentReducer变量中。

currentState,用于记录当前状态,应用的状态就保存在这里。

currentListeners存储的监听器,当状态发生改变时,里面的监听器会被调用,nextListeners是用于辅助currentListeners被改变时的变量。

isDispatching用于确保redux已经在执行dispatch任务时,不会再执行其他dispatch任务。

这些内部变量并不能直接操作,但是可以通过createStore暴露出来的方法对它们进行操作,所以createStore其实也是一个大的闭包。

然后我们先跳过中间的大段代码,直接看最后createStore返回的是什么。

1
2
3
4
5
6
7
8
9
// ...
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
// ...

可以看到熟悉的dispatch,subscibe都在这里,这几个是核心方法,现在我们来逐个分析。

dispatch

首先是dispatch,它是改变状态的关键函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ...
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
}
// ...

我们都知道,dispatch接受一个action,然后它会根据我们写的对应的reducer返回的新的状态对象,来替换原来的状态。

源码中,前3个if,分别用于判断传入的action是否是纯对象,确定action.type是否存在,以及确定当前没有在执行其他dispatch任务,如果不符合相关要求,则抛出相关错误。

后面这一步我们很熟悉,将当前的状态和动作传入reducer中,执行后返回的结果就是新的状态。

接着遍历所有的注册的监听器,将所有监听器都执行一遍。

最后返回当前的action。

subscribe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ...
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)
}
}
// ...

subscribe方法用于注册一个监听器,这些监听器会在状态发生改变时被触发。

首先检查传入的监听器是否是函数类型,不是则抛出错误,然后将isSubscribed设置为true,该变量保证当我们需要调用unsubscribe函数的时候不会重复注销。

ensureCanMutateNextListeners这个方法保证了当前任务不会改变现有的监听器,注意redux中有两个监听器的数组,一个是currentListeners,另一个是nextListeners,ensureCanMutateNextListeners保证了这两个数组不是同一个引用。

1
2
3
4
5
6
7
// ...
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
// ...

为什么要这么做呢?我们可以从subscribe的注释中找到答案。

1
2
3
4
5
6
7
8
9
/**
* ...
* 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.
* ...
*/

译文:所有的注册在调用dispatch之前都被拍了快照,如果你在监听器在被调用的时候注册或者注销任何监听器,也不会影响到当前正在执行的dispatch。当然,下一次调用dispatch的时候,无论是否嵌套,redux都会使用更新的注册列表快照。

ensureCanMutateNextListeners就是做这个的。

subscribe方法最终返回了一个unsubscribe方法,该方法用于注销当前注册的监听器。

其他

剩下的3个方法,getState直接返回当前状态,replaceReducer用于动态加载reducer(热加载),observable用于和响应式编程库一起使用(rxJs之类的)。

在createStore.js文件一开始,定义了一个action,它用于做初始化。

1
2
3
4
5
// ...
export const ActionTypes = {
INIT: '@@redux/INIT'
}
// ...

在createStore函数返回5个方法组成的对象之前,redux触发了这个动作,

1
2
3
// ...
dispatch({ type: ActionTypes.INIT })
// ...

这样当我们创建一个store的时候,就会有一个我们再reducer里面声明好的默认状态存储在store中。

以上就是createStore的核心代码分析,正如本节一开始说的,这个文件是代码最多的一个,却相对非常简单,接下来我们就来看一个代码很少,却有些难理解的文件。

compose

1
2
3
4
5
6
7
8
9
10
11
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}

if (funcs.length === 1) {
return funcs[0]
}

return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

这就是compose.js的全部代码(除去注释),那该函数用于实现什么效果呢?

compose是被其他文件依赖的一个函数,它其实和redux的主要逻辑相对比较独立,所以我们可以抛开redux来对其进行分析。

函数接收的所有参数都被spread运算符放在funcs对象中,可以将其理解为函数的内置对象arguments,但和arguments不同的是,funcs确实是一个真的数组。此外从funcs这个名字可以看出,compose函数接收的参数都要求是函数类型的。

compose内部就3句代码:

  1. 如果参数funcs一个元素都没有,则返回一个无意义的函数;
  2. 如果参数funcs只有一个元素,则返回该函数;
  3. 其他情况下:???
1
return funcs.reduce((a, b) => (...args) => a(b(...args)))

这行代码乍一看很吓人,但是抽丝剥茧,我们很快就能理解这个黑魔法到底是做什么的。

首先数组的reduce方法,相信我们都不陌生,我们没有看到reduce方法给了第2个参数,也就是说没有默认值,那数组的第1个参数就是第一个a参数了。

假设funcs中有2个函数:

1
2
3
const a = arg => arg + 1;
const b = arg => arg + 2;
compose(a, b);

这里我故意将两个函数的名字取名为a和b,这样方便我们直接代入reduce中,这个时候返回的就是一个新的函数:

1
(...args) => a(b(...args));

也就是说如果我这时候传入3进去,则3先交给b函数处理,返回结果是5,结果再交给a处理,返回结果是6(好像有点头绪了)。

如果现在有3个函数呢?

1
2
3
4
const a = arg => arg + 1;
const b = arg => arg + 2;
const c = arg => arg + 3;
compose(a, b, c);

这个时候reduce的参数方法会执行两次,第1次的结果我们已经分析过了,是:

1
(...args) => a(b(...args));

那第2次呢?此时的参数a是上面这个第1次调用的记过函数,而参数b实际上是c函数,最后的结果就是:

1
(...args) => a(b(c(...args)));

现在来归纳总结一下,实际上compose函数就是将我们传入的所有函数嵌套起来调用,返回一个按照顺序嵌套好的函数,该函数接收的参数会交给compose(参数都是函数)的最后一个参数处理,处理的结果再交给倒数第二个参数处理,再交给倒数第三个,依次类推。

简单地理解就是,原来我们可能要写:

1
a(b(c(d(...yourArgs))));

现在可以改写为:

1
compose(a, b, c, d)(...yourArgs);

compose的代码看上去似乎很少,初理解起来确实会困难一些。

那说了那么多,compose到底在redux的什么地方用到呢?接下来又是一个我们熟悉的API。

applyMiddleware

在学习applyMiddleware之前,我们先来看一下官方的示例,回顾一下applyMiddleware在redux中是怎么使用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* Logs all actions and states after they are dispatched.
*/
const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd(action.type)
return result
}

/**
* Sends crash reports as state is updated and listeners are notified.
*/
const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}

let store = createStore(
todoApp,
applyMiddleware(
logger,
crashReporter
)
)

首先创建两个中间件,logger和crashReporter,然后在调用createStore时配合applyMiddleware以应用中间件。

前面说过createStore接收3个参数,第2个参数是初始化默认状态,第3个参数是enhancer(如果初始化默认状态缺失时,则第2个参数作为enhancer),用于扩展redux。

那么createStore中是怎么处理enhancer的呢?

1
2
3
4
5
6
7
8
9
// ...
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}

return enhancer(createStore)(reducer, preloadedState)
}
// ...

如果enhancer存在且为函数,则返回enhancer(createStore)(reducer, preloadedState)。为了理解这句代码,我们需要去看看applyMiddleware中的代码了(applyMiddleware就是最常见的enhancer)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import compose from './compose'

export default function applyMiddleware(...middlewares) {
return (createStore) => (...args) => {
const store = createStore(...args)
let dispatch = store.dispatch
let chain = []

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

return {
...store,
dispatch
}
}
}

是的,这就是applyMiddleware的所有代码了(除去注释)。整个函数是一个闭包,返回了一个函数,闭包内保存的私有变量只有传进来的所有参数。

返回的函数接收createStore作为参数,可以看到createStore中enhancer(createStore)(reducer, preloadedState)也是这么处理的,将自身传进了applyMiddleware返回的函数中,然后使用enhancer(createStore)(reducer, preloadedState)创建一个store,这个store是不带任何enhancer的store。

现在applyMiddleware可以对store进行处理了,首先构建了一个middlewareAPI对象,实际上就是将store中的getState和dispatch两个方法拿出来单独构建了一个对象,这样做是为了对所有中间件屏蔽store的其他方法(如subscribe)

接下来是就是最重要的两行代码了。

1
2
3
4
// ...
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
// ...

第一行代码实际上是把自定义的中间件抽出以next为参数的函数出来,因为我们把middlewareAPI当做store传入之后,返回的实际上是这样一个函数:

1
2
3
4
5
6
7
8
next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd(action.type)
return result
}

所有的中间件经过这样的处理之后,然后我们把返回的结果都放到名为chain的数组中。接下来就是见证奇迹的时刻了,compose函数终于登场了,还记得上面的分析吗?compose函数可以函数的嵌套调用形式进行转换。

1
2
3
a(b(c(d(...yourArgs))));
// 等价于
compose(a, b, c, d)(...yourArgs);

同样地,logger和crashReporter也会被这样处理,最终转换结果是怎样的呢?

1
2
3
dipsatch = compose(...chain)(store.dispatch)
// 等价于
dipsatch = logger(middlewareAPI)(crashReporter(middlewareAPI)(store.dispatch))

最后返回的dispatch,才是经过各种中间件包装之后的我们想要的dispatch。

1
2
3
4
5
6
// ...
return {
...store,
dispatch
}
// ...

这里的spread运算符将store展开后虽然有一个原本的dispatch,但是后面的新的dispatch会将其覆盖。

这样,经过applyMiddleware处理之后,原始的dispatch就被各种各样的中间件包裹了起来,我们也就能依靠中间件在redux上添加一些自己想要的功能了。

combineReducers

有的时候我们无法将所有的状态处理都放到一个reducer中处理,这样会造成reducer的代码臃肿且难以管理,redux提供的combineReducers就帮助了我们处理这个问题,它可以将多个reducer绑定为一个reducer。具体用法如下。

1
2
3
4
5
6
const reducer = combineReducers({
todos: TodoList.reducer,
filter: Filter.reducer
});

export default createStore(reducer);

上面的TodoList.reducer和Filter.reducer都是合法的reducer,combineReducers接收一个对象作为参数,对象的属性名需要自己取(一般和子reducer一一对应),值是其他的reducer。由于最终还是要传给createStore去用的,所以combineReducers返回的结果必然也必须是一个合法的reducer(能让redux正常工作)。

combineReducers的源码较多,我们捡关键的来分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ...
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]
}
}
// ...
}

首先来看开头,这一部分的代码主要是对传进来的reducers进行预处理,要求参数reducers对象中的每一个属性的值都不能是空,否则在开发环境下会抛出错误,最终所有合法的属性和值(值要求是函数)都会放到一个新的名为finalReducers的对象中去。

为了保证自定义的reducer符合redux的要求,redux对所有的reducer都进行了简单的验证。

1
2
3
4
5
6
7
8
// ...
let shapeAssertionError
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
// ...

这里的assertRedcuerShape,源码就不列出来分析了,简单地讲就是做了两个验证,一是当初始化的时候(也就是没有任何状态传入的时候)必须返回一个非undefined的状态,二是当触发一个随机类型的动作时也必须返回一个非undefined的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ...
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的返回结果肯定是一个函数,因为该函数必须是将reducers组合过之后的总的reducer。

最终的reducer先保证reducers是合法的,然后如果是在开发环境下,还会通过getUnexpectedStateShapeWarningMessage函数对传入的状态,reducer,action做进一步的检查,该函数做了如下检查。

  1. 保证待组合的reducers的长度大于1;
  2. 保证传入的状态是普通对象(原型是null或者Object.prototype);
  3. 保证传入的状态的属性名和待组合的reducers的属性名是对应的。

简单地讲,就是开发环境下保证各个参数是合法的,否则就会抛出错误。

最后的for循环就是真正开始组合的地方,对于每一个子reducer,都将前一个状态中的对应的子状态和当前action传入子reducer中,计算下一个状态,如果下一个状态是undefined,则抛出错误,否则将其保存在nextState对应的key中,同时,还有一个记录当前各个子状态是否发生改变的变量hasChanged,如果有任何一个子reducer改变它的子状态,hasChanged都会被修改为true,最后如果状态发生了改变,则返回新状态,如果没有发生改变,则返回之前的状态。

即使当前状态和下一状态完全相等,我们也不能直接返回下一状态,因为这两个状态是不同引用,所以相等情况下,也应该返回当前状态。这种处理不仅方便了我们在React中优化时做状态的对比,而且更重要地是redux中的所有监听器的触发是依赖于浅比较的,只有两个状态的引用不一样时,监听器才会被触发。

还有一点值得说明的是,通过combineReducers合并多个子reducer,各个子reducer无法相互访问状态,而如果整个store只有一个reducer则不存在这种问题。

bindActionCreators

最后一个方法是bindActionCreators,先来看看它的官方用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const TodoActionCreators = {
addTodo(text) {
return {
type: 'ADD_TODO',
text
}
},
removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
}
}
};

let boundActionCreators = bindActionCreators(TodoActionCreators, dispatch)
console.log(boundActionCreators)
// {
// addTodo: Function,
// removeTodo: Function
// }

最终输出的对象,里面的Function是什么呢?实际上就是调用boundActionCreators.addTodo(‘Hello World’)等价于dispatch(TodoActionCreator.addTodo(‘Hello World’)),这种做法在React组件中传递属性时就很方便了。

1
2
3
4
5
render() {
// ...
return <TodoList todos={todos} {...boundActionCreators} />
// ...
}

了解了用法之后,现在来看看源码是怎么做的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function bindActionCreator(actionCreator, dispatch) {
return function() { return dispatch(actionCreator.apply(this, arguments)) }
}

export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}

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"?`
)
}

const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}

这是将所有注释去掉之后的源码,bindActionCreators接收两个参数,第一个是构造action的函数组成的对象,第二个是store的dispatch。

为了方便,如果actionCreators只是一个单一的构造action的函数,则直接返回一个可执行dispatch该action的匿名函数。

然后做检查,保证actionCreators是一个对象,最后循环对每一个actionCreator进行绑定,绑定后返回的执行dispatch的函数和相应的属性一一对应,最后返回绑定好的对象,这个API也相对比较简单。

总结

本文将几乎所有的redux源码都详细分析了一遍(除了util中的warning函数和其他的一些验证参数合法性的代码之外),希望能对想深入了解React及生态系统中相关库的同学有一定帮助,redux的API不多,代码虽然少,但是设计库时的一些黑魔法还是很值得学习。

最后如果文中有谬误的地方,还请指正。

参考

  1. reactjs/redux - GitHub