当一个应用足够大的时候,咱们使用一个reducer
函数来维护state
是会碰到麻烦的,太过庞大,分支会不少,想一想都会恐怖。基于以上这一点,redux
支持拆分reducer
,每一个独立的reducer
管理state
树的某一块。html
随着应用变得愈来愈复杂,能够考虑将 reducer 函数 拆分红多个单独的函数,拆分后的每一个函数负责独立管理 state 的一部分。combineReducers 辅助函数的做用是,把一个由多个不一样 reducer 函数做为 value 的 object,合并成一个最终的 reducer 函数,而后就能够对这个 reducer 调用 createStore 方法。vue
根据redux
文档介绍,来看一下这个函数的实现。node
export default function combineReducers(reducers) {
...
return function combination(state = {}, action) {
...
}
}
复制代码
先看一下函数的结构,就如文档所说,传入一个key-value
对象,value
为拆分的各个reducer
,而后返回一个reducer
函数,就如代码里面的combination
函数,看入参就知道和reducer
函数一致。git
检查的操做就是在返回以前,看看代码。github
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)
// This is used to make sure we don't warn about the same
// keys multiple times.
let unexpectedKeyCache
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
let shapeAssertionError
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
复制代码
Object.keys
拿到入参对象的key
,而后声明一个finalReducers
变量用来存方最终的reducer
。reducerKeys
,检查每一个reducer
的正确性,好比控制的判断,是否为函数的判断,若是符合规范就放到finalReducerKeys
对象中。Object.keys
获取清洗后的key
assertReducerShape(finalReducers)
函数去检查每一个reducer
的预期返回值,它应该符合如下:
通过了检查,最终返回了reducer
函数,相比咱们直接写reducer
函数,这里面预置了一些操做,重点就是来协调各个reducer
的返回值。vuex
if (shapeAssertionError) {
throw shapeAssertionError
}
if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
复制代码
若是以前检查有警告或者错误,在执行reducer
的时候就直接抛出。编程
最后在调用dispatch
函数以后,处理state
的代码以下:redux
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
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state
复制代码
isChanged
来表示,通过reducer
处理以后,state
是否变动了。finalReducerKeys
。reducer
和对应的key
而且根据key
获取到state
相关的子树。reducer(previousStateForKey, action)
获取对应的返回值。undefined
,而后进行相应的报错。key
中。===
进行比较新获取的值和state
里面的旧值,能够看到这里只是比较了引用,注意redcuer
里面约束有修改都是返回一个新的state
,全部若是你直接修改旧state
引用的话,这里的hasChanged
就会被判断为false
,在下一步中,若是为false
就会返回旧的state
,数据就不会变化了。hasChanged
判断返回原始值仍是新值。当咱们须要使用异步处理state
的时候,因为reducer
必需要是纯函数,这和redux
的设计理念有关,为了能够能追踪到每次state
的变化,reducer
的每次返回值必须是肯定的,才能追踪到。具体放在后面讲。api
当使用中间件,咱们须要经过applyMiddleware
去整合中间件,而后传入到createStore
函数中,这时候相应的流程会发生变化。数组
先看看createStore
函数对这部分的处理。
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
复制代码
这里的enhancer
就是applyMiddleware(thunk, logger, ...)
执行后的返回值。能够看到,enhancer
函数执行,须要把createStore
函数传入,说明enhancer
内部会本身去处理一些其余操做后,再回来调用createStore
生成store
。
首先看一下applyMiddleware
的结构。
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
...
}
}
复制代码
能够看到applyMiddleware
函数啥都没干,只是对传入的middlewares
参数造成了一个闭包,把这个变量缓存起来了。确实很函数式。
接下来看一下它的返回的这个函数:
createStore => (...args) => {}
复制代码
它返回的这个函数也只是把createStore
缓存(柯里化绑定)了下来,目前在createStore
执行到了这一步enhancer(createStore)
。
再看看这个返回函数执行后的返回函数。
(...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
复制代码
createStore
传入reducer, preloadedState
这两个参数,也就是...args
,生成store
。dispatch
为一个只会抛错误的空函数。middlewareAPI
变量,对象里面有两个属性,分别为getState
和dispatch
,这里的dispatch
是一个函数,执行的时候会调用当前做用域的dispatch
变量,能够看到,在这一步dispatch
仍是那个空函数。middlewares
,将构建的middlewareAPI
变量传入,生成一个新的队列,里面装的都是各个中间件执行后的返回值(通常为函数)。compose
去生成新的dispatch
函数。store
的全部属性返回,而后使用新生成的dispatch
去替换默认的dispatch
函数。中间件的重点就是将dispatch
替换成了新生成的dispatch
函数,以致于能够在最后调用store.dispatch
以前作一些其余的操做。生成的核心在于compose
函数,接下来看看。
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)))
}
复制代码
reduce
方法,对这些参数(函数),进行一种整合。看看官方注释:
For example, compose(f, g, h) is identical to doing (...args) => f(g(h(...args))). 这就是为何像
logger
这样的中间件须要注意顺序的缘由了,若是放在最后一个参数。最后一个中间件能够拿到最终的store.dispatch
,全部能在它的先后记录变动,不受其余影响。nodejs
的koa
框架的洋葱模型与之相似。
再回到applyMiddleware
函数,通过compose
函数处理后,最后返回了一个函数。
compose(...chain)(store.dispatch)
复制代码
再把store.dispatch
传入到这些整合后的中间件后,获得最后的dispatch
函数。
看了redux
是怎么处理整合中间件的,看一下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;
复制代码
能够看到最终导出的是createThunkMiddleware
函数的返回值,这就是中间件的一个实现了。
store
,也就是applyMiddleware
函数在执行 const chain = middlewares.map(middleware => middleware(middlewareAPI))
会传入的。compose(...chain)(store.dispatch)
函数获得的,这里会将其余的中间件做为参数next
传入。能够看到,当传入的action
为函数的时候,直接就return
了,打断了中间件的pie
执行,而是去执行了action
函数里面的一些异步操做,最后异步成功或者失败了,又从新调用dispatch
,从新启动中间件的pie
。
上面说到,为何reducer
为何必定须要是纯函数?下面说说我的理解。
经过源码,能够反应出来。hasChanged = hasChanged || nextStateForKey !== previousStateForKey ... return hasChanged ? nextState : state
。
从这一点能够看到,是否变化redux
只是简单的使用了精确等于来判断的,若是reducer
是直接修改旧值,那么这里的判断会将修改后的丢弃掉了。那么为何redux
要这么设计呢?我在网上查了一些文章,说的最多的就是说,若是想要判断A、B
两对象是否相对,就只能深度对比每个属性,咱们知道redux
应用在大型项目上,state
的结构会很庞大,变动频率也是很高的,每次都进行深度比较,消耗很大。全部redux
就把这个问题给抛给开发者了。
还有为何reducer
或者vuex
里面的mutation
中,不能执行异步操做,引用·vuex
官方文档:
Mutation 必须是同步函数 一条重要的原则就是要记住 mutation 必须是同步函数。为何?请参考下面的例子:
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
复制代码
如今想象,咱们正在 debug 一个 app 而且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都须要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:由于当 mutation 触发的时候,回调函数尚未被调用,devtools 不知道何时回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
文档地址,reducer
也是同理。
几行代码能够作不少事情,好比中间件的串联实现,函数式的编程使人眼花缭乱。
分析了combineReducer
和applyMiddleware
,redux
也就梳理完了。中间件的编程思想很值得借鉴,在中间件上下相互不知的状况下,也能很好的协做。
参考文章