2018年4月22日更新
GitHub上的redux源码已经更新几个小版本,虽然有一些细节上的区别,但是核心代码逻辑还是大同小异。
概述
本文较长,希望能对redux实现感兴趣的同学有一定的帮助。
redux是React生态系统中非常重要的一个存在,它是React最流行的状态管理库。redux的源码并不多,如果你打开redux的源码,你会发现它其实只有7个文件,加上注释一共也只有500多行代码,所以就算阅读它全部的源码也不会太费工夫。
基本结构
redux是开源的,我们可以在GitHub(参考1)上找到所有的源码。
1 | ── src |
这就是redux的全部源码,可以看到,文件名大都是我们熟悉的API。
入口文件index.js我们不分析,实际上它就是检查了一下当前执行的环境(开发环境还是生产环境),然后将其他各个文件引入再导出为一个整体的对象。
createStore
当我们使用redux时,一般是在写完actionTypes,actions,reducers,然后就从调用createStore开始创建一个store,所以我们就先拿它开刀。
createStore.js是这几个文件中代码量最多的一个(当前版本共255行),但是逻辑却是非常简单。
createStore本身是一个大的闭包,函数接收3个参数。
1 | // ... |
reducer我们都很熟悉,redux依据reducer来构建和修改应用的状态,preloadedState用于指定一个默认的状态,注意如果我们使用了combineReducers这个API,则默认提供的preloadedState的键也要和combineReducers中的键一一对应,否则会报错,enhancer则而已用于扩展和修改redux的默认功能,最常见的例子就是applyMiddleware了。
其实createStore在创建store的时候会dispatch一个默认的action,该action也会触发reducer,reducer会返回指定的默认状态,store就会将其当做初始状态,所以第2个参数preloadedState好像并没有什么用(不确定是不是我理解错了),所以一般情况下建议,在reducer中声明默认返回值当做初始状态。
函数一开始对参数做了类型检查,并且如果参数有缺失,则对参数顺序进行调整,然后定义了几个内部变量。
1 | // ... |
上面的reducer,就是传进到createStore方法中的reducer,reducer使redux修改状态树的关键依据,它被保存在了currentReducer变量中。
currentState,用于记录当前状态,应用的状态就保存在这里。
currentListeners存储的监听器,当状态发生改变时,里面的监听器会被调用,nextListeners是用于辅助currentListeners被改变时的变量。
isDispatching用于确保redux已经在执行dispatch任务时,不会再执行其他dispatch任务。
这些内部变量并不能直接操作,但是可以通过createStore暴露出来的方法对它们进行操作,所以createStore其实也是一个大的闭包。
然后我们先跳过中间的大段代码,直接看最后createStore返回的是什么。
1 | // ... |
可以看到熟悉的dispatch,subscibe都在这里,这几个是核心方法,现在我们来逐个分析。
dispatch
首先是dispatch,它是改变状态的关键函数。
1 | // ... |
我们都知道,dispatch接受一个action,然后它会根据我们写的对应的reducer返回的新的状态对象,来替换原来的状态。
源码中,前3个if,分别用于判断传入的action是否是纯对象,确定action.type是否存在,以及确定当前没有在执行其他dispatch任务,如果不符合相关要求,则抛出相关错误。
后面这一步我们很熟悉,将当前的状态和动作传入reducer中,执行后返回的结果就是新的状态。
接着遍历所有的注册的监听器,将所有监听器都执行一遍。
最后返回当前的action。
subscribe
1 | // ... |
subscribe方法用于注册一个监听器,这些监听器会在状态发生改变时被触发。
首先检查传入的监听器是否是函数类型,不是则抛出错误,然后将isSubscribed设置为true,该变量保证当我们需要调用unsubscribe函数的时候不会重复注销。
ensureCanMutateNextListeners这个方法保证了当前任务不会改变现有的监听器,注意redux中有两个监听器的数组,一个是currentListeners,另一个是nextListeners,ensureCanMutateNextListeners保证了这两个数组不是同一个引用。
1 | // ... |
为什么要这么做呢?我们可以从subscribe的注释中找到答案。
1 | /** |
译文:所有的注册在调用dispatch之前都被拍了快照,如果你在监听器在被调用的时候注册或者注销任何监听器,也不会影响到当前正在执行的dispatch。当然,下一次调用dispatch的时候,无论是否嵌套,redux都会使用更新的注册列表快照。
ensureCanMutateNextListeners就是做这个的。
subscribe方法最终返回了一个unsubscribe方法,该方法用于注销当前注册的监听器。
其他
剩下的3个方法,getState直接返回当前状态,replaceReducer用于动态加载reducer(热加载),observable用于和响应式编程库一起使用(rxJs之类的)。
在createStore.js文件一开始,定义了一个action,它用于做初始化。
1 | // ... |
在createStore函数返回5个方法组成的对象之前,redux触发了这个动作,
1 | // ... |
这样当我们创建一个store的时候,就会有一个我们再reducer里面声明好的默认状态存储在store中。
以上就是createStore的核心代码分析,正如本节一开始说的,这个文件是代码最多的一个,却相对非常简单,接下来我们就来看一个代码很少,却有些难理解的文件。
compose
1 | export default function compose(...funcs) { |
这就是compose.js的全部代码(除去注释),那该函数用于实现什么效果呢?
compose是被其他文件依赖的一个函数,它其实和redux的主要逻辑相对比较独立,所以我们可以抛开redux来对其进行分析。
函数接收的所有参数都被spread运算符放在funcs对象中,可以将其理解为函数的内置对象arguments,但和arguments不同的是,funcs确实是一个真的数组。此外从funcs这个名字可以看出,compose函数接收的参数都要求是函数类型的。
compose内部就3句代码:
- 如果参数funcs一个元素都没有,则返回一个无意义的函数;
- 如果参数funcs只有一个元素,则返回该函数;
- 其他情况下:???
1 | return funcs.reduce((a, b) => (...args) => a(b(...args))) |
这行代码乍一看很吓人,但是抽丝剥茧,我们很快就能理解这个黑魔法到底是做什么的。
首先数组的reduce方法,相信我们都不陌生,我们没有看到reduce方法给了第2个参数,也就是说没有默认值,那数组的第1个参数就是第一个a参数了。
假设funcs中有2个函数:
1 | const a = arg => arg + 1; |
这里我故意将两个函数的名字取名为a和b,这样方便我们直接代入reduce中,这个时候返回的就是一个新的函数:
1 | (...args) => a(b(...args)); |
也就是说如果我这时候传入3进去,则3先交给b函数处理,返回结果是5,结果再交给a处理,返回结果是6(好像有点头绪了)。
如果现在有3个函数呢?
1 | const a = arg => arg + 1; |
这个时候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 | /** |
首先创建两个中间件,logger和crashReporter,然后在调用createStore时配合applyMiddleware以应用中间件。
前面说过createStore接收3个参数,第2个参数是初始化默认状态,第3个参数是enhancer(如果初始化默认状态缺失时,则第2个参数作为enhancer),用于扩展redux。
那么createStore中是怎么处理enhancer的呢?
1 | // ... |
如果enhancer存在且为函数,则返回enhancer(createStore)(reducer, preloadedState)。为了理解这句代码,我们需要去看看applyMiddleware中的代码了(applyMiddleware就是最常见的enhancer)。
1 | import compose from './compose' |
是的,这就是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 | // ... |
第一行代码实际上是把自定义的中间件抽出以next为参数的函数出来,因为我们把middlewareAPI当做store传入之后,返回的实际上是这样一个函数:
1 | next => action => { |
所有的中间件经过这样的处理之后,然后我们把返回的结果都放到名为chain的数组中。接下来就是见证奇迹的时刻了,compose函数终于登场了,还记得上面的分析吗?compose函数可以函数的嵌套调用形式进行转换。
1 | a(b(c(d(...yourArgs)))); |
同样地,logger和crashReporter也会被这样处理,最终转换结果是怎样的呢?
1 | dipsatch = compose(...chain)(store.dispatch) |
最后返回的dispatch,才是经过各种中间件包装之后的我们想要的dispatch。
1 | // ... |
这里的spread运算符将store展开后虽然有一个原本的dispatch,但是后面的新的dispatch会将其覆盖。
这样,经过applyMiddleware处理之后,原始的dispatch就被各种各样的中间件包裹了起来,我们也就能依靠中间件在redux上添加一些自己想要的功能了。
combineReducers
有的时候我们无法将所有的状态处理都放到一个reducer中处理,这样会造成reducer的代码臃肿且难以管理,redux提供的combineReducers就帮助了我们处理这个问题,它可以将多个reducer绑定为一个reducer。具体用法如下。
1 | const reducer = combineReducers({ |
上面的TodoList.reducer和Filter.reducer都是合法的reducer,combineReducers接收一个对象作为参数,对象的属性名需要自己取(一般和子reducer一一对应),值是其他的reducer。由于最终还是要传给createStore去用的,所以combineReducers返回的结果必然也必须是一个合法的reducer(能让redux正常工作)。
combineReducers的源码较多,我们捡关键的来分析。
1 | // ... |
首先来看开头,这一部分的代码主要是对传进来的reducers进行预处理,要求参数reducers对象中的每一个属性的值都不能是空,否则在开发环境下会抛出错误,最终所有合法的属性和值(值要求是函数)都会放到一个新的名为finalReducers的对象中去。
为了保证自定义的reducer符合redux的要求,redux对所有的reducer都进行了简单的验证。
1 | // ... |
这里的assertRedcuerShape,源码就不列出来分析了,简单地讲就是做了两个验证,一是当初始化的时候(也就是没有任何状态传入的时候)必须返回一个非undefined的状态,二是当触发一个随机类型的动作时也必须返回一个非undefined的状态。
1 | // ... |
combineReducers的返回结果肯定是一个函数,因为该函数必须是将reducers组合过之后的总的reducer。
最终的reducer先保证reducers是合法的,然后如果是在开发环境下,还会通过getUnexpectedStateShapeWarningMessage函数对传入的状态,reducer,action做进一步的检查,该函数做了如下检查。
- 保证待组合的reducers的长度大于1;
- 保证传入的状态是普通对象(原型是null或者Object.prototype);
- 保证传入的状态的属性名和待组合的reducers的属性名是对应的。
简单地讲,就是开发环境下保证各个参数是合法的,否则就会抛出错误。
最后的for循环就是真正开始组合的地方,对于每一个子reducer,都将前一个状态中的对应的子状态和当前action传入子reducer中,计算下一个状态,如果下一个状态是undefined,则抛出错误,否则将其保存在nextState对应的key中,同时,还有一个记录当前各个子状态是否发生改变的变量hasChanged,如果有任何一个子reducer改变它的子状态,hasChanged都会被修改为true,最后如果状态发生了改变,则返回新状态,如果没有发生改变,则返回之前的状态。
即使当前状态和下一状态完全相等,我们也不能直接返回下一状态,因为这两个状态是不同引用,所以相等情况下,也应该返回当前状态。这种处理不仅方便了我们在React中优化时做状态的对比,而且更重要地是redux中的所有监听器的触发是依赖于浅比较的,只有两个状态的引用不一样时,监听器才会被触发。
还有一点值得说明的是,通过combineReducers合并多个子reducer,各个子reducer无法相互访问状态,而如果整个store只有一个reducer则不存在这种问题。
bindActionCreators
最后一个方法是bindActionCreators,先来看看它的官方用法:
1 | const TodoActionCreators = { |
最终输出的对象,里面的Function是什么呢?实际上就是调用boundActionCreators.addTodo(‘Hello World’)等价于dispatch(TodoActionCreator.addTodo(‘Hello World’)),这种做法在React组件中传递属性时就很方便了。
1 | render() { |
了解了用法之后,现在来看看源码是怎么做的。
1 | function bindActionCreator(actionCreator, dispatch) { |
这是将所有注释去掉之后的源码,bindActionCreators接收两个参数,第一个是构造action的函数组成的对象,第二个是store的dispatch。
为了方便,如果actionCreators只是一个单一的构造action的函数,则直接返回一个可执行dispatch该action的匿名函数。
然后做检查,保证actionCreators是一个对象,最后循环对每一个actionCreator进行绑定,绑定后返回的执行dispatch的函数和相应的属性一一对应,最后返回绑定好的对象,这个API也相对比较简单。
总结
本文将几乎所有的redux源码都详细分析了一遍(除了util中的warning函数和其他的一些验证参数合法性的代码之外),希望能对想深入了解React及生态系统中相关库的同学有一定帮助,redux的API不多,代码虽然少,但是设计库时的一些黑魔法还是很值得学习。
最后如果文中有谬误的地方,还请指正。