Open fi3ework opened 6 years ago
加一句,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 => {
//...
}
}
说明
本文所分析的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的目录结构:
源码分析
源码分析的顺序推荐如下,就是跟着pipeline的顺序来
主题思路我会写出来,很细节的部分就直接写在代码注释里了。
index
只有两个功能:
createStore
createStore 由于有两种生成 store 的方法,所以起手先确定各个参数
然后声明中间变量,后面会讲到这些中间变量的作用
然后看怎么订阅一个事件,其实这就是一个发布-订阅模式,但是和普通的发布订阅模式不同的是, 多了一个
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拷贝赋值,即只在必要时拷贝。接下来看 dispatch 这个函数,可以看到每次dispatch时会
const listeners = (currentListeners = nextListeners)
,为可能到来的mutateNextListener做好准备。接下来看
getState
,就是一个returnobservable函数,这个是为了配合RxJS使用,如果不使用RxJS可以忽略,在这里略过。
replaceReducer函数是替换整个store的reducer,一般不经常用到,代码也含简单,换个reducer重新init一下
最后,暴露的接口的功能都已经具备了,还需要取一下默认值,你可能会说不是已经有preloadedState了吗但是默认值不是只有一个的,每个reducer都可以指定对应部分的state的默认值,那些默认值需要先经过一个action的洗礼才可以被赋值,还记得reducer要求每个不可识别的action.type返回原始state吗?就是为了取得默认值。
为了保证这个type是无法识别的,被定义成了一个随机值
至此,我们的已经能够createStore,getState,subscribe,unsubscribe,dispatch了
combineReducer
combineReducer的代码挺长的,但是主要都是用来检查错误了,核心代码就是将要合并的代码组织组织成一个树结构,然后将传入的reduce挨个跑action,跑出的新的state替换掉原来的state,因为无法识别的action会返回原来的state,所以大部分无关的reducer会返回相同引用的state,只有真正捕获action的reducer会返回新的state,这样做到了局部更新,否则每次state的一部分更新导致所有的state都原地深拷贝一次就麻烦了。
bindActionCreators
这个用的不多,一般是为了方便,直接
import *
来引入多个actionCreators,原理很简单:实际上就是返回一个高阶函数,通过闭包引用,将 dispatch 给隐藏了起来,正常操作是发起一个 dispatch(action),但bindActionCreators 将 dispatch 隐藏,当执行bindActionCreators返回的函数时,就会dispatch(actionCreators(...arguments))。所以参数叫做 actionCreators,作用是返回一个 action 如果是一个对象里有多个 actionCreators 的话,就会类似 map 函数返回一个对应的对象,每个 key 对应的 value 就是上面所说的被绑定了的函数。applyMiddleware
精髓来了,这个函数最短但是最精髓,这个 middleware 的洋葱模型的思想是从koa的中间件拿过来的,我没看过koa的中间件(因为我连koa都没用过...),但是重要的是思想。
放上redux的洋葱模型的示意图(via 中间件的洋葱模型)
单独理解太晦涩,放一个最简单的
redux-thunk
帮助理解。redux-thunk:
最难理解的就是那三个柯里化箭头,这三个箭头相当于欠了真正的middleware实体三个参数
{dispatch, getState}
,next
,action
,作为一个中间件,就是将这几个像管道一样在各个中间件中传递,与此同时加上一些副作用,比如后续管道的走向或者发起异步请求等等。那么开始看欠的这三个参数是怎么还给中间件的,代码不长,所以直接写在注释里了,一行一行看就可以。
applyMiddleware: