Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
bruce-16 opened this issue Aug 13, 2019 · 0 comments
Open

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

bruce-16 opened this issue Aug 13, 2019 · 0 comments

Comments

@bruce-16
Copy link
Owner

当一个应用足够大的时候,我们使用一个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
}
  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的返回值。

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
  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的每次返回值必须是确定的,才能追踪到。具体放在后面讲。

当使用中间件,我们需要通过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的洋葱模型
@bruce-16 bruce-16 changed the title Redux 梳理分析【二】 Redux 梳理分析【二:combineReducers和中间件】 Aug 13, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant