阅读对象:使用过redux,对redux实现原理不是很理解的开发者。javascript
在我实习入职培训的时候,给我培训的老哥就跟我说过,redux的核心源码很简洁,建议我有空去看一下,提高对redux系列的理解。php
入职一个多月了,已经参与了公司的很多项目,redux也使用了一段时间,对于redux的理解却一直没有深刻,还停留在“知道怎么用,可是不知道其核心原理”的阶段。java
因此就在github上拉了redux的源码,看了一会,发现东西确实很少,比较简洁。react
在项目中,咱们每每不会纯粹的使用redux,而是会配合其余的一些工具库提高效率,好比
react-redux
,让react应用使用redux更容易,相似的也有wepy-redux
,提供给小程序框架wepy的工具库。git
可是在本文中,咱们讨论的范围就纯粹些,仅仅讨论redux自己。github
redux自己有哪些做用?咱们先来快速的过一下redux的核心思想(工做流程):编程
在这个工做流程中,redux须要提供的功能是:redux
createStore()
subscribe
,dispatch
,getState
这些方法。combineReducers()
applyMiddleware()
没错,就这么多功能,咱们看下redux的源码目录:小程序
确实也就这么多,至于compose
,bindActionCreators
,则是一些工具方法。数组
下面咱们就逐个来看看createStore
、combineReducers
、applyMiddleware
、compose
的源码实现。
建议打开连接:redux源码地址,参照本文的解释阅读源码。
这个函数的大体结构是这样:
function createStore(reducer, preloadedState, enhancer) {
if(enhancer是有效的){ // 这个咱们后面会解释,能够先忽略
return enhancer(createStore)(reducer, preloadedState)
}
let currentReducer = reducer // 当前store中的reducer
let currentState = preloadedState // 当前store中存储的状态
let currentListeners = [] // 当前store中放置的监听函数
let nextListeners = currentListeners // 下一次dispatch时的监听函数
// 注意:当咱们新添加一个监听函数时,只会在下一次dispatch的时候生效。
//...
// 获取state
function getState() {
//...
}
// 添加一个监听函数,每当dispatch被调用的时候都会执行这个监听函数
function subscribe() {
//...
}
// 触发了一个action,所以咱们调用reducer,获得的新的state,而且执行全部添加到store中的监听函数。
function dispatch() {
//...
}
//...
//dispatch一个用于初始化的action,至关于调用一次reducer
//而后将reducer中的子reducer的初始值也获取到
//详见下面reducer的实现。
return {
dispatch,
subscribe,
getState,
//下面两个是主要面向库开发者的方法,暂时先忽略
//replaceReducer,
//observable
}
}
复制代码
能够看出,createStore方法建立了一个store,可是并无直接将这个store的状态state返回,而是返回了一系列方法,外部能够经过这些方法(getState)获取state,或者间接地(经过调用dispatch)改变state。
至于state呢,被存在了闭包中。(不理解闭包的同窗能够先去了解一下先)
咱们再来详细的看看每一个模块是如何实现的(为了让逻辑更清晰,省略了错误处理的代码):
function getState() {
return currentState
}
复制代码
简单到发指。其实这很像面向对象编程中封装只读属性的方法,只提供数据的getter方法,而不直接提供setter。(虽然这里返回的是一个state的引用,你能够直接修改state,可是通常来讲,redux不建议这样作。)
function subscribe(listener) {
// 添加到监听函数数组,
// 注意:咱们添加到了下一次dispatch时才会生效的数组
nextListeners.push(listener)
let isSubscribe = true //设置一个标志,标志该监听器已经订阅了
// 返回取消订阅的函数,即从数组中删除该监听函数
return function unsubscribe() {
if(!isSubscribe) {
return // 若是已经取消订阅过了,直接返回
}
isSubscribe = false
// 从下一轮的监听函数数组(用于下一次dispatch)中删除这个监听器。
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
复制代码
subscribe返回的是一个取消订阅的方法。取消订阅是很是必要的,当添加的监听器没用了以后,应该从store中清理掉。否则每次dispatch都会调用这个没用的监听器。
function dispatch(action) {
//调用reducer,获得新state
currentState = currentReducer(currentState, action);
//更新监听数组
currentListener = nextListener;
//调用监听数组中的全部监听函数
for(let i = 0; i < currentListener.length; i++) {
const listener = currentListener[i];
listener();
}
}
复制代码
createStore这个方法的基本功能咱们已经实现了,可是调用createStore方法须要提供reducer,让咱们来思考一下reducer的做用。
在理解combineReducers以前,咱们先来想一想reducer的功能:reducer接受一个旧的状态和一个action,当这个action被触发的时候,reducer处理后返回一个新状态。
也就是说 ,reducer负责状态的管理(或者说更新)。在实际使用中,咱们应用的状态是能够分红不少个模块的,好比一个典型社交网站的状态能够分为:用户我的信息,好友列表,消息列表等模块。理论上,咱们能够用一个reducer去处理全部状态的维护,可是这样作的话,咱们一个reducer函数的逻辑就会太多,容易产生混乱。
所以咱们能够将逻辑(reducer)也按照模块划分,每一个模块再细分红各个子模块,开发完每一个模块的逻辑后,再将reducer合并起来,这样咱们的逻辑就能很清晰的组合起来。
对于咱们的这种需求,redux提供了combineReducers方法,能够把子reducer合并成一个总的reducer。
来看看redux源码中combineReducers的主要逻辑:
function combineReducers(reducers) {
//先获取传入reducers对象的全部key
const reducerKeys = Object.keys(reducers)
const finalReducers = {} // 最后真正有效的reducer存在这里
//下面从reducers中筛选出有效的reducer
for(let i = 0; i < reducerKeys.length; i++){
const key = reducerKeys[i]
if(typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers);
//这里assertReducerShape函数作的事情是:
// 检查finalReducer中的reducer接受一个初始action或一个未知的action时,是否依旧可以返回有效的值。
let shapeAssertionError
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
//返回合并后的reducer
return function combination(state= {}, action){
//这里的逻辑是:
//取得每一个子reducer对应的state,与action一块儿做为参数给每一个子reducer执行。
let hasChanged = false //标志state是否有变化
let nextState = {}
for(let i = 0; i < finalReducerKeys.length; i++) {
//获得本次循环的子reducer
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
//获得该子reducer对应的旧状态
const previousStateForKey = state[key]
//调用子reducer获得新状态
const nextStateForKey = reducer(previousStateForKey, action)
//存到nextState中(总的状态)
nextState[key] = nextStateForKey
//到这里时有一个问题:
//就是若是子reducer不能处理该action,那么会返回previousStateForKey
//也就是旧状态,当全部状态都没改变时,咱们直接返回以前的state就能够了。
hasChanged = hasChanged || previousStateForKey !== nextStateForKey
}
return hasChanged ? nextState : state
}
}
复制代码
在redux的设计思想中,reducer应该是一个纯函数
维基百科关于纯函数的定义:
在程序设计中,若一个函数符合如下要求,则它可能被认为是纯函数:
- 此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值之外的其余隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
- 该函数不能有语义上可观察的函数反作用,诸如“触发事件”,使输出设备输出,或更改输出值之外物件的内容等。
纯函数的输出能够不用和全部的输入值有关,甚至能够和全部的输入值都无关。但纯函数的输出不能和输入值之外的任何资讯有关。纯函数能够传回多个输出值,但上述的原则需针对全部输出值都要成立。若引数是传引用调用,如有对参数物件的更改,就会影响函数之外物件的内容,所以就不是纯函数。
总结一下,纯函数的重点在于:
Math.random
,Date.now
这些方法影响输出)reducer为何要求使用纯函数,文档里也有提到,总结下来有这几点:
state是根据reducer建立出来的,因此reducer是和state紧密相关的,对于state,咱们有时候须要有一些需求(好比打印每一次更新先后的state,或者回到某一次更新前的state)这就对reducer有一些要求。
纯函数更易于调试
若是不使用纯函数,那么在比较新旧状态对应的两个对象时,咱们就不得不深比较了,深比较是很是浪费性能的。相反的,若是对于全部可能被修改的对象(好比reducer被调用了一次,传入的state就可能被改变),咱们都新建一个对象并赋值,两个对象有不一样的地址。那么浅比较就能够了。
至此,咱们已经知道了,reducer是一个纯函数,那么若是咱们在应用中确实须要处理一些反作用(好比异步处理,调用API等操做),那么该怎么办呢?这就是中间件解决的问题。下面咱们就来说讲redux中的中间件。
中间件在redux中位于什么位置,咱们能够经过这两张图来看一下。
先来看看不用中间件时的redux工做流程:
而用了中间件以后的工做流程是这样的:
那么中间件该如何融合到redux中呢?
在上面的流程中,2-4的步骤是关于中间件的,但凡咱们想要添加一个中间件,咱们就须要写一套2-4的逻辑。
若是咱们须要多个中间件,咱们就须要考虑如何让他们串联起来。若是每次串联都写一份串联逻辑的话,就不够灵活,万一须要增删改或调整中间件的顺序,都须要修改中间件串联的逻辑。
因此redux提供了一种解决方案,将中间件的串联操做进行了封装,通过封装后,上面的步骤2-5就能够成为一个总体,以下图:
咱们只须要改造store自带的dispatch方法。action发生后,先给中间件处理,最后再dispatch一个action交给reducer去改变状态。
还记得redux 的createStore()
方法的第三个参数enhancer
吗:
function createStore(reducer, preloadedState, enhancer) {
if(enhancer是有效的){
return enhancer(createStore)(reducer, preloadedState)
}
//...
}
复制代码
在这里,咱们能够看到,enhancer(能够叫作强化器)是一个函数,这个函数接受一个「普通createStore函数」做为参数,返回一个「增强后的createStore函数」。
这个增强的过程当中作的事情,其实就是改造dispatch,添加上中间件。
redux提供的applyMiddleware()
方法返回的就是一个enhancer。
applyMiddleware,顾名思义,「应用中间件」。输入为若干中间件,输出为enhancer。下面来看看它的源码:
function applyMiddleware(...middlewares) {
// 返回一个函数A,函数A的参数是一个createStore函数。
// 函数A的返回值是函数B,其实也就是一个增强后的createStore函数,大括号内的是函数B的函数体
return createStore => (...args) => {
//用参数传进来的createStore建立一个store
const store = createStore(...args)
//注意,咱们在这里须要改造的只是store的dispatch方法
let dispatch = () => { //一个临时的dispatch
//做用是在dispatch改造完成前调用dispatch只会打印错误信息
throw new Error(`一些错误信息`)
}
//接下来咱们准备将每一个中间件与咱们的state关联起来(经过传入getState方法),获得改造函数。
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
//middlewares是一个中间件函数数组,中间件函数的返回值是一个改造dispatch的函数
//调用数组中的每一个中间件函数,获得全部的改造函数
const chain = middlewares.map(middleware => middleware(middlewareAPI))
//将这些改造函数compose(翻译:构成,整理成)成一个函数
//用compose后的函数去改造store的dispatch
dispatch = compose(...chain)(store.dispatch)
// compose方法的做用是,例如这样调用:
// compose(func1,func2,func3)
// 返回一个函数: (...args) => func1( func2( func3(...args) ) )
// 即传入的dispatch被func3改造后获得一个新的dispatch,新的dispatch继续被func2改造...
// 返回store,用改造后的dispatch方法替换store中的dispatch
return {
...store,
dispatch
}
}
}
复制代码
总结一下,applyMiddleware的工做方式是:
中间件的工做方式是:
getState
和dispatch
,输出为改造函数(改造dispatch
的函数)dispatch
,输出「改造后的dispatch
」源码中用到了一个颇有用的方法:compose()
,将多个函数组合成一个函数。理解这个函数对理解中间件颇有帮助,咱们来看看它的源码:
function compose(...funcs) {
// 当未传入函数时,返回一个函数:arg => arg
if(funcs.length === 0) {
return arg => arg
}
// 当只传入一个函数时,直接返回这个函数
if(funcs.length === 1) {
return funcs[0]
}
// 返回组合后的函数
return funcs.reduce((a, b) => (...args) => a(b(...args)))
//reduce是js的Array对象的内置方法
//array.reduce(callback)的做用是:给array中每个元素应用callback函数
//callback函数:
/* *@参数{accumulator}:callback上一次调用的返回值 *@参数{value}:当前数组元素 *@参数{index}:可选,当前元素的索引 *@参数{array}:可选,当前数组 * *callback( accumulator, value, [index], [array]) */
}
复制代码
画一张图来理解compose的做用:
在applyMiddleware方法中,咱们传入的「参数」是原始的dispatch方法,返回的「结果」是改造后的dispatch方法。经过compose,咱们可让多个改造函数抽象成一个改造函数。
做者注:原本只想讲redux,可是讲着讲着却发现:理解中间件,是理解redux的中间件机制的前提。
下面咱们以redux-thunk为例,看看一个中间件是如何实现的。
你可能没用过redux-thunk,因此在阅读源码前,我先简要的讲一下redux-thunk的做用:
正常的dispatch函数的参数action应该是一个纯对象。像这样:
store.dispatch({
type:'REQUEST_SOME_THING',
payload: {
from:'bob',
}
})
复制代码
使用了thunk以后,咱们能够dispatch一个函数:
function logStateInOneSecond(name) {
return (dispatch, getState, name) => { // 这个函数会在合适的时候dispatch一个真正的action
setTimeout({
console.log(getState())
dispatch({
type:'LOG_OK',
payload: {
name,
}
})
}, 1000)
}
}
store.dispatch(logStateInOneSecond('jay')) //dispatch的参数是一个函数
复制代码
为何须要这个功能?或者说「dispatch一个函数」能解决什么问题?
从上面的例子中你会发现,若是dispatch一个函数,咱们能够在这个函数内作任何咱们想要的操做(异步处理,调用接口等等),不受任何限制,为何?
由于咱们「尚未dispatch一个真正的action」,因此不会调用reducer,咱们并无将反作用放在reducer中,而是在使用reducer以前就处理了反作用。
若是你还不明白redux-thunk的功能,能够去它的github仓库查看更详细的解释。
如何实现redux-thunk中间件呢?
首先中间件确定是改造dispatch方法,改造后的dispatch应该具备这样的功能:
如今咱们来看看redux-thunk的源码(8行有效代码):
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
复制代码
若是三个箭头函数让你有点头晕,我来帮你展开一下:
//createThunkMiddleware的做用是返回thunk中间件(middleware)
function createThunkMiddleware(extraArgument) {
return function({ dispatch, getState }) { // 这是「中间件函数」
return function(next) { // 这是中间件函数建立的「改造函数」
return function(action) { // 这是改造函数改造后的「dispatch方法」
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
}
}
}
}
复制代码
再加点注释?
function createThunkMiddleware(extraArgument) {
return function({ dispatch, getState }) { // 这是「中间件函数」
//参数是store中的dispatch和getState方法
return function(next) { // 这是中间件函数建立的「改造函数」
//参数next是被当前中间件改造前的dispatch
//由于在被当前中间件改造以前,可能已经被其余中间件改造过了,因此不妨叫next
return function(action) { // 这是改造函数「改造后的dispatch方法」
if (typeof action === 'function') {
//若是action是一个函数,就调用这个函数,并传入参数给函数使用
return action(dispatch, getState, extraArgument);
}
//不然调用用改造前的dispatch方法
return next(action);
}
}
}
}
复制代码
讲完了。能够看出redux-thunk严格遵循了redux中间件的思想:在原始的dispatch方法触发reducer处理以前,处理反作用。
至此,redux的核心源码已经讲完了,最后不得不感叹,redux写的真的美,真tm的简洁。
一句话总结redux的核心功能:「建立一个store来管理state」
关于中间件,我会尝试着写一篇《如何本身实现一个redux中间件》,更深刻的理解redux中间件的意义。
关于store如何与其余框架(如react)共同工做,我会再写一篇《react-redux
源码解读》的博客探究探究这个问题。
敬请期待。
compose()
的源码解释unSubscribe
的解释redux-thunk
为例