当一个应用足够大的时候,我们使用一个reducer函数来维护state是会碰到麻烦的,太过庞大,分支会很多,想想都会恐怖。基于以上这一点,redux支持拆分reducer,每个独立的reducer管理state树的某一块。
combineReducers 函数
随着应用变得越来越复杂,可以考虑将 reducer 函数 拆分成多个单独的函数,拆分后的每个函数负责独立管理 state 的一部分。combineReducers 辅助函数的作用是,把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore 方法。
根据redux文档介绍,来看一下这个函数的实现。
export default function combineReducers(reducers) {
...
return function combination(state = {}, action) {
...
}
}
先看一下函数的结构,就如文档所说,传入一个key-value对象,value为拆分的各个reducer,然后返回一个reducer函数,就如代码里面的combination函数,看入参就知道和reducer函数一致。
检查传入的 reducers 对象的合理性
检查的操作就是在返回之前,看看代码。
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的预期返回值,它应该符合以下:- 所有未匹配到的 action,必须把它接收到的第一个参数也就是那个 state 原封不动返回。
- 永远不能返回 undefined。当过早 return 时非常容易犯这个错误,为了避免错误扩散,遇到这种情况时 combineReducers 会抛异常。
- 如果传入的 state 就是 undefined,一定要返回对应 reducer 的初始 state。
combination 函数
经过了检查,最终返回了reducer函数,相比我们直接写reducer函数,这里面预置了一些操作,重点就是来协调各个reducer的返回值。
if (shapeAssertionError) {
throw shapeAssertionError
}
if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
如果之前检查有警告或者错误,在执行reducer的时候就直接抛出。
最后在调用dispatch函数之后,处理state的代码如下:
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的每次返回值必须是确定的,才能追踪到。具体放在后面讲。
当使用中间件,我们需要通过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 函数
首先看一下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函数。
compose 函数
中间件的重点就是将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)))
}
- 如果参数的长度为0,就返回兜底一个函数,这函数只会把传入的形参返回,没有其他操作。
- 如果参数的长度为1,就将这个元素返回。
- 这个情况就是说有多个参数,然后调用数组的
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-thunk 中间件
看了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也就梳理完了。中间件的编程思想很值得借鉴,在中间件上下相互不知的情况下,也能很好的协作。
参考文章