Redux 梳理分析【二:combineReducers和中间件】

当一个应用足够大的时候,咱们使用一个reducer函数来维护state是会碰到麻烦的,太过庞大,分支会不少,想一想都会恐怖。基于以上这一点,redux支持拆分reducer,每一个独立的reducer管理state树的某一块。html

combineReducers 函数

随着应用变得愈来愈复杂,能够考虑将 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

检查传入的 reducers 对象的合理性

检查的操做就是在返回以前,看看代码。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
}
复制代码
  1. 使用Object.keys拿到入参对象的key,而后声明一个finalReducers变量用来存方最终的reducer
  2. 遍历reducerKeys,检查每一个reducer的正确性,好比控制的判断,是否为函数的判断,若是符合规范就放到finalReducerKeys对象中。
  3. 使用Object.keys获取清洗后的key
  4. 经过assertReducerShape(finalReducers)函数去检查每一个reducer的预期返回值,它应该符合如下:
    1. 全部未匹配到的 action,必须把它接收到的第一个参数也就是那个 state 原封不动返回。
    2. 永远不能返回 undefined。当过早 return 时很是容易犯这个错误,为了不错误扩散,遇到这种状况时 combineReducers 会抛异常。
    3. 若是传入的 state 就是 undefined,必定要返回对应 reducer 的初始 state。

combination 函数

通过了检查,最终返回了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
复制代码
  1. 声明一个变量isChanged来表示,通过reducer处理以后,state是否变动了。
  2. 遍历 finalReducerKeys
  3. 获取reducer和对应的key而且根据key获取到state相关的子树。
  4. 执行reducer(previousStateForKey, action)获取对应的返回值。
  5. 判断返回值是否为undefined,而后进行相应的报错。
  6. 将返回值赋值到对应的key中。
  7. 使用===进行比较新获取的值和state里面的旧值,能够看到这里只是比较了引用,注意redcuer里面约束有修改都是返回一个新的state,全部若是你直接修改旧state引用的话,这里的hasChanged就会被判断为false,在下一步中,若是为false就会返回旧的state,数据就不会变化了。
  8. 最后遍历完以后,经过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 函数

首先看一下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
  }
}
复制代码
  1. 调用createStore传入reducer, preloadedState这两个参数,也就是...args,生成store
  2. 声明变量dispatch为一个只会抛错误的空函数。
  3. 构造 middlewareAPI变量,对象里面有两个属性,分别为getStatedispatch,这里的dispatch是一个函数,执行的时候会调用当前做用域的dispatch变量,能够看到,在这一步dispatch仍是那个空函数。
  4. 遍历传入的middlewares,将构建的middlewareAPI变量传入,生成一个新的队列,里面装的都是各个中间件执行后的返回值(通常为函数)。
  5. 经过函数 compose 去生成新的dispatch函数。
  6. 最后把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)))
}
复制代码
  1. 若是参数的长度为0,就返回兜底一个函数,这函数只会把传入的形参返回,没有其余操做。
  2. 若是参数的长度为1,就将这个元素返回。
  3. 这个状况就是说有多个参数,而后调用数组的reduce方法,对这些参数(函数),进行一种整合。看看官方注释:

    For example, compose(f, g, h) is identical to doing (...args) => f(g(h(...args))). 这就是为何像logger这样的中间件须要注意顺序的缘由了,若是放在最后一个参数。最后一个中间件能够拿到最终的store.dispatch,全部能在它的先后记录变动,不受其余影响。nodejskoa框架的洋葱模型与之相似。

再回到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函数的返回值,这就是中间件的一个实现了。

  1. 第一个函数,获得的是store,也就是applyMiddleware函数在执行 const chain = middlewares.map(middleware => middleware(middlewareAPI)) 会传入的。
  2. 第二个函数,是在compose(...chain)(store.dispatch)函数获得的,这里会将其余的中间件做为参数next传入。
  3. 第三个函数,就是用来实现本身的逻辑了,拦截或者进行日志打印。

能够看到,当传入的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也是同理。

小结

几行代码能够作不少事情,好比中间件的串联实现,函数式的编程使人眼花缭乱。

分析了combineReducerapplyMiddlewareredux也就梳理完了。中间件的编程思想很值得借鉴,在中间件上下相互不知的状况下,也能很好的协做。

参考文章

  1. 图解Redux中middleware的洋葱模型

原文地址

相关文章
相关标签/搜索