通过Github Blame深入分析Redux源码
fi3ework opened this issue · comments
说明
本文所分析的Redux版本为3.7.2
分析直接写在了注释里,放在了GitHub上 —> 仓库地址
分析代码时通过查看Github blame,参考了Redux的issue及PR来分析各个函数的意图而不仅是从代码层面分析函数的作用,并且分析了很多细节层面上写法的原因,比如:
-
dispatch: (...args) => dispatch(…args)
为什么不只传递一个action
? -
listener
的调用为什么从forEach
改成了for
? -
为什么在
reducer
的调用过程中不允许dispatch(action)
?...
水平有限,有写的不好或不对的地方请指出,欢迎留issue交流😆
文件结构
Redux的文件结构并不复杂,每个文件就是一个对外导出的函数,依赖很少,分析起来也比较容易,只要会用Redux基本上都能看懂本文。
这是Redux的目录结构:
.
├── applyMiddleware.js 将middleware串联起来生成一个更强大的dispatch函数,就是中间件的本质作用
├── bindActionCreators.js 把action creators转成拥有同名keys的对象
├── combineReducers.js 将多个reducer组合起来,每一个reducer独立管理自己对应的state
├── compose.js 将middleware从右向左依次调用,函数式编程中的常用方法,被applyMiddleware调用
├── createStore.js 最核心功能,创建一个store,包括实现了subscribe, unsubscribe, dispatch及state的储存
├── index.js 对外export
└── utils 一些小的辅助函数供其他的函数调用
├── actionTypes.js redux内置的action,用来初始化initialState
├── isPlainObject.js 用来判断是否为单纯对象
└── warning.js 报错提示
源码分析
源码分析的顺序推荐如下,就是跟着pipeline的顺序来
index.js -> createStore.js -> applyMiddleware.js (compose.js) -> combineReducers.js -> bindActionCreators.js
主题思路我会写出来,很细节的部分就直接写在代码注释里了。
index
function isCrushed () {}
// 如果使用minified的redux代码会降低性能。
// 这里的isCrushed函数主要是为了验证在非生产环境下的redux代码是否被minified
// 如果被压缩了那么isCrushed.name !== 'isCrushed'
if (
process.env.NODE_ENV !== 'production' &&
typeof isCrushed.name === 'string' && // 有的浏览器(IE)并不支持Function.name,必须判断先判断是否支持Function.name,才能判断是否minified
isCrushed.name !== 'isCrushed'
) {
warning(
"...'
)
}
export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose,
__DO_NOT_USE__ActionTypes
}
只有两个功能:
- 区分开发环境和生产环境
- 对外暴露API,相当简洁,常用的API只有五个
createStore
createStore 由于有两种生成 store 的方法,所以起手先确定各个参数
// 传递两个参数时,实际传递的是 reducer 和 enhancer,preloadedState 为 undefined
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
// 传递三个参数时,传递的是 reducer preloadedState enhancer(enhancer必须为函数)
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
// 如果传入了 enhancer(一个组合 store creator 的高阶函数)则控制反转,交由enhancer来加强要生成的store
// 再对这个加强后的 store 传递 reducer 和 preloadedState
return enhancer(createStore)(reducer, preloadedState)
}
// 传入的reducer必须是一个纯函数,且是必填参数
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
然后声明中间变量,后面会讲到这些中间变量的作用
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
然后看怎么订阅一个事件,其实这就是一个发布-订阅模式,但是和普通的发布订阅模式不同的是, 多了一个ensureCanMutateNextListeners
函数。 我去翻了一下redux的commit message,找到了对listener做深拷贝的原因:https://github.com/reactjs/redux/issues/461,简单来说就是在listener中可能有unsubscribe操作,比如有3个listener(下标0,1,2),在第2个listener执行时unsubscribe了自己,那么第3个listener的下标就变成了1,但是for循环下一轮的下标是2,第3个listener就被跳过了,所以执行一次深拷贝,即使在listener过程中unsubscribe了也是更改的nextListeners(nextListeners会去深拷贝currentListeners)。当前执行的currentListeners不会被修改,也就是所谓的快照。
redux在执行subscribe和unsubscribe的时候都要执行ensureCanMutateNextListeners来确定是否要进行一次深拷贝,只要执行dispatch,那么就会被const listeners = (currentListeners = nextListeners)
,所以currentListeners === nextListeners,之后的subscribe和unsubscribe就必须深拷贝一次, 否则可以一直对nextListeners操作而不需要为currentListeners拷贝赋值,即只在必要时拷贝。
function subscribe (listener) {
// 传入的listener必须是一个可以调用的函数,否则报错
if (typeof listener !== 'function') {
throw new Error('Expected listener to be a function.')
}
// 同上,保证纯函数不带来副作用
if (isDispatching) {
throw new Error(
'...'
)
}
let isSubscribed = true
// 在每次subscribe的时候,nextListenerx先拷贝currentListeners,再push新的listener
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe () {
if (!isSubscribed) {
return
}
// 同上,保证纯函数不带来副作用
if (isDispatching) {
throw new Error(
'...'
)
}
isSubscribed = false
// 在每次unsubscribe的时候,深拷贝一次currentListeners,再对nextListeners取消订阅当前listener
ensureCanMutateNextListeners()
// 从nextListeners中去掉unsubscribe的listener
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
接下来看 dispatch 这个函数,可以看到每次dispatch时会const listeners = (currentListeners = nextListeners)
,为可能到来的mutateNextListener做好准备。
function dispatch (action) {
// action必须是一个plain object,如果想要能处理传进来的函数的话必须使用中间件(redux-thunk等)
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
// action必须定义type属性
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.')
}
// currentReducer不可预料是否会报错,所以try,但不catch
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
// 必须在结束的时候将isDispatching归位
isDispatching = false
}
// 在这里体现了currentListeners和nextListeners的作用
const listeners = (currentListeners = nextListeners)
// 这里使用for而不是forEach,是因为listeners是我们自己创造的,不存在稀疏组的情况,所有直接用for性能来得更好
// 见 https://github.com/reactjs/redux/commit/5b586080b43ca233f78d56cbadf706c933fefd19
// 附上Dan的原话:This is an optimization because forEach() has more complicated logic per spec to deal with sparse arrays. Also it's better to not allocate a function when we can easily avoid that.
// 这里没有缓存listeners.length,Dan相信V8足够智能会自动缓存,相比手工缓存性能更好
for (let i = 0; i < listeners.length; i++) {
// 这里将listener单独新建一个变量而不是listener[i]()
// 是因为直接listeners[i]()会把listeners作为this泄漏,而赋值为listener()后this指向全局变量
// https://github.com/reactjs/redux/commit/8e82c15f1288a0a5c5c886ffd87e7e73dc0103e1
const listener = listeners[i]
listener()
}
return action
}
接下来看getState
,就是一个return
function getState () {
// 参考:https://github.com/reactjs/redux/issues/1568
// 为了保持reducer的pure,禁止在reducer中调用getState
// 纯函数reducer要求根据一定的输入即能得到确定的输出,所以禁止了getState,subscribe,unsubscribe和dispatch等会带来副作用的行为
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
}
observable函数,这个是为了配合RxJS使用,如果不使用RxJS可以忽略,在这里略过。
replaceReducer函数是替换整个store的reducer,一般不经常用到,代码也含简单,换个reducer重新init一下
function replaceReducer (nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer
// ActionTypes.REPLACE其实就是ActionTypes.INIT
// 重新INIT依次是为了获取新的reducer中的默认参数
dispatch({ type: ActionTypes.REPLACE })
}
最后,暴露的接口的功能都已经具备了,还需要取一下默认值,你可能会说不是已经有preloadedState了吗但是默认值不是只有一个的,每个reducer都可以指定对应部分的state的默认值,那些默认值需要先经过一个action的洗礼才可以被赋值,还记得reducer要求每个不可识别的action.type返回原始state吗?就是为了取得默认值。
// reducer要求对无法识别的action返回state,就是因为需要通过ActionTypes.INIT获取默认参数值并返回
// 当initailState和reducer的参数默认值都存在的时候,参数默认值将不起作用
// 因为在调用初始化的action前currState就已经被赋值了initialState
// 同时这个initialState也是服务端渲染的初始状态入口
dispatch({ type: ActionTypes.INIT })
为了保证这个type是无法识别的,被定义成了一个随机值
const ActionTypes = {
// INIT和REPLACE一模一样,只是含义不同,REPLACE其实就是INIT
INIT:
'@@redux/INIT' +
Math.random()
.toString(36)
.substring(7)
.split('')
.join('.'),
REPLACE:
'@@redux/REPLACE' +
Math.random()
.toString(36)
.substring(7)
.split('')
.join('.')
}
至此,我们的已经能够createStore,getState,subscribe,unsubscribe,dispatch了
combineReducer
combineReducer的代码挺长的,但是主要都是用来检查错误了,核心代码就是将要合并的代码组织组织成一个树结构,然后将传入的reduce挨个跑action,跑出的新的state替换掉原来的state,因为无法识别的action会返回原来的state,所以大部分无关的reducer会返回相同引用的state,只有真正捕获action的reducer会返回新的state,这样做到了局部更新,否则每次state的一部分更新导致所有的state都原地深拷贝一次就麻烦了。
export default function combineReducers (reducers) {
// 第一次筛选:将reducers中为function的属性赋值给finalReducers
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 = {}
}
// 用来检查reducer是否会返回undefined
// 因为combineReducers有可能会嵌套多层,当嵌套的某一层如果返回undefined
// 那么当访问这一层的子reducer的时候就会发生TypeError的错误
let shapeAssertionError
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
// combination:组合起来的reducer
return function combination (state = {}, action) {
// 如果之前的reducer检查不合法,则throw错误
if (shapeAssertionError) {
throw shapeAssertionError
}
// 检查excepted state并打印错误
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)
// 不允许任何action返回undefined
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
bindActionCreators
这个用的不多,一般是为了方便,直接import *
来引入多个actionCreators,原理很简单:实际上就是返回一个高阶函数,通过闭包引用,将 dispatch 给隐藏了起来,正常操作是发起一个 dispatch(action),但bindActionCreators 将 dispatch 隐藏,当执行bindActionCreators返回的函数时,就会dispatch(actionCreators(...arguments))。所以参数叫做 actionCreators,作用是返回一个 action
如果是一个对象里有多个 actionCreators 的话,就会类似 map 函数返回一个对应的对象,每个 key 对应的 value 就是上面所说的被绑定了的函数。
// 真正需要获取参数的函数被柯里化了起来
function bindActionCreator (actionCreator, dispatch) {
// 高阶函数,闭包引用 dispatch
return function () {
return dispatch(actionCreator.apply(this, arguments))
}
}
export default function bindActionCreators (actionCreators, dispatch) {
// 如果是actionCreators是函数,那么直接调用,比如是个需要被thunk的函数�
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error(
`...`
)
}
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') {
// 每个 key 再次调用一次 bindActionCreator
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
// map 后的对象
return boundActionCreators
}
applyMiddleware
精髓来了,这个函数最短但是最精髓,这个 middleware 的洋葱模型的**是从koa的中间件拿过来的,我没看过koa的中间件(因为我连koa都没用过...),但是重要的是**。
放上redux的洋葱模型的示意图(via 中间件的洋葱模型)
--------------------------------------
| middleware1 |
| ---------------------------- |
| | middleware2 | |
| | ------------------- | |
| | | middleware3 | | |
| | | | | |
next next next ——————————— | | |
dispatch —————————————> | reducer | — 收尾工作->|
nextState <————————————— | G | | | |
| A | C | E ——————————— F | D | B |
| | | | | |
| | ------------------- | |
| ---------------------------- |
--------------------------------------
顺序 A -> C -> E -> G -> F -> D -> B
\---------------/ \----------/
↓ ↓
更新 state 完毕 收尾工作
单独理解太晦涩,放一个最简单的redux-thunk
帮助理解。
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;
最难理解的就是那三个柯里化箭头,这三个箭头相当于欠了真正的middleware实体三个参数{dispatch, getState}
, next
, action
,作为一个中间件,就是将这几个像管道一样在各个中间件中传递,与此同时加上一些副作用,比如后续管道的走向或者发起异步请求等等。
那么开始看欠的这三个参数是怎么还给中间件的,代码不长,所以直接写在注释里了,一行一行看就可以。
applyMiddleware:
export default function applyMiddleware (...middlewares) {
// 传入createStore
return createStore => (...args) => {
// 先用传入的createStore来创建一个最普通的store
const store = createStore(...args)
// 初始化dispatch,记住这个dispatch是最终我们要将各个中间件串联起来的dispatch
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
// 储存将要被串联起来的中间件,函数签名为next => action => {...}
// 下一个中间件作为next传进去,被当前中间件调用
let chain = []
const middlewareAPI = {
getState: store.getState,
// 在这里dispatch使用匿名函数是为了能在middleware中调用最新的dispatch(闭包):
// 必须是匿名函数而不是直接写成dispatch: store.dispatch
// 这样能保证在middleware中传入的dispatch都通过闭包引用着最终compose出来的dispatch
// 如果直接写成store.dispatch,在`dispatch = compose(...chain)(store.dispatch)`中
// middlewareAPI.dispatch并没有得到更新,依旧是最老的,只有在最后才得到了更新
// 但是我们要保证在整个中间件的调用过程中,任何中间件调用的都是最终的dispatch
// 我写了个模拟的调用,可以在 http://jsbin.com/fezitiwike/edit?js,console 上感受一下
// 还有,这里使用了...args而不是action,是因为有个PR https://github.com/reactjs/redux/pull/2560
// 这个PR的作者认为在dispatch时需要提供多个参数,像这样`dispatch(action, option)`
// 这种情况确实存在,但是只有当这个需提供多参数的中间件是第一个被调用的中间件时(即在middlewares数组中排最后)才有效
// 因为无法保证上一个调用这个多参数中间件的中间件是使用的next(action)或是next(...args)来调用
// 在这个PR的讨论中可以看到Dan对这个改动持保留意见
dispatch: (...args) => dispatch(...args)
}
// 还了 {dispatch, getState}
chain = middlewares.map(middleware => middleware(middlewareAPI))
// 还了 next
// 最开始的 next 就是 store.dispatch
// 相当于就是每个中间件在自己的过程中做一些操作,做完之后调用下一个中间件(next(action))
dispatch = compose(...chain)(store.dispatch)
// 最终返回一个dispatch被修改了的store,这个dispatch串联起了中间件
// 欠的那个action会在dispatch的时候传入
return {
...store,
dispatch
}
}
}
加一句,redux的执行流程与koa的一样,但是在middleware里再调用dispatch就是重置当前任务流 A->B->C(调用dispatch) ->A->B->D
C的那步必须做条件判断,否则死循环,类似的使用场景有
const loggerHistory = store => {
history.listen((location) => {
store.dispatch({type: 'history', payload: location})
})
return next => action => {
//...
}
}