Redux系列之👉ApplyMiddleWare

温馨提示: 阅读本文需了解Redux和Redux的MiddleWare。

我们知道,在redux中,我们dispatch一个action后,reducer函数会被调用,返回一个新的state

但我们有时并不满足于此,我们可能会想要在action到达reducer前的间隙做某些额外的操作,比如打印日志、异步请求等。而这些额外的功能就被称为miidleWare(中间件)。有了中间件我们的dispatch流程就变成了这样:

OK,介绍完middleWare了!我们来从0开始,思考一个midlleWare的诞生过程。

有一天,你在被代码调试逼疯的边缘,你突然想在每次dispatch以后打印新的state,于是你想当然地写出下面的代码:

1
2
3
const action = toggleNum('2');
store.dispatch(action);
console.log(`nextState 👉`store.getState());

开心还没过两秒,你突然脑筋一动,这是一次性代码啊!如果以后在别的地方也需要这个功能的话,难不成要复制 -> 粘贴吗?于是你封装了如下函数:

1
2
3
4
const dispatchAndLog = (store, action) => {
  store.dispatch(action);
  console.log(`nextState 👉`store.getState());
}

到目前为止,一切似乎都在预期中,可当你写的代码越来越多时,你发现每次都要在文件中把这个函数import进来。你脑袋里冒出了这个想法:"干脆直接重写dispatch函数算了,这样以后就不用每次都笨拙地import这个函数了!",于是你甩开袖子开始干:

1
2
3
4
5
const next = store.dispatch;
store.dispatch = action => {
  next(action);
  console.log(`nextState 👉`store.getState());
}

稍微解释一下上面的代码,以防真的有人不懂为什么要存在const next = store.dispatch;这一行代码。这是利用了Javascript的闭包特性,引用(记住)了最原始的dispatch函数,然后在重写的dispatch函数内调用它。

根据啥都要封装的特点,你把上面的代码封装进函数,同时你还有另一个功能也要在action到达reducer被使用,于是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 打印nextState功能
const patchStoreToLogging = store => {
  const next = store.dispatch;
  store.dispatch = action => {
    const result = next(action);
    console.log(`nextState 👉`store.getState());
    return result;
  }
}

// 记录报错功能
const patchStoreToAddCrashReporting = store => {
  const next = store.dispatch
  store.dispatch = action => {
    try {
      return next(action)
    } catch (err) {
      console.error('Caught an exception!', err)
      throw err
    }
  }
}

封装完这两个函数,你开心坏了,因为你这下子只需要执行上面两个函数以后dispatch函数就拥有了记录nextState报错功能。可是,你还是觉得哪里有些不够好。哦!你灵光一闪,如果现在我们有了第三个功能(中间件),我们还需要像调用上面这两个函数一样再次调用第三个函数,还是太蠢了呀!能否有一个util函数,我把我的中间件传给它,它自动帮我遍历这些中间件,然后依次应用到dispatch上呢?

在写这个util函数前,你决定先把上面的两个函数改造一下。为什么要改造呢,我们可以看到每次函数里面都是直接给store.dispatch重新赋值,这样是可以的,但是我们不如把赋值右边的函数给return出去,为什么这样做?因为这样,我们便可以链式调用这些middleWares,更加函数式了!:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 打印nextState功能
const patchStoreToLogging = store => {
  const next = store.dispatch;
  //之前是用store.dispatch = 
  // 现在我们直接把该函数return出去!
  return action => {
    const result = next(action);
    console.log(`nextState 👉`store.getState());
    return result;
  }
}

终于,你可以来封装这个util函数了(也就是applyMiddleWare)!:

1
2
3
4
5
6
7
8
const applyMiddleWare = (store, middleWares) => {
  const copiedMiddles = middleWares.slice();
  copiedMiddles.reverse();

  copiedMiddles.forEach(middleWare => {
    store.dispatch = middleWare(store);
  })
}

解释上面代码之前,有必要强调两件事情:

  • 这个applyMiddleWare函数在做的事情是: 改写dispatch函数!
  • 函数里面的middleWare(store)返回的是增强后的dispatch函数!

OK,现在可以说一下const copiedMiddles = middleWares.slice();copiedMiddles.reverse();这两句是在干啥了。我们传入的middleWares数组是有顺序这一概念的,什么意思呢?比如说我们的middleWares = [m1, m2, m3];,当我们真正在dispatch一个action的时候,我们期望的顺序是m1先被调用,然后是m2,最后是m3。但是我们在copiedMiddles.forEach(middleWare => {store.dispatch = middleWare(store);})这两句代码中,m1先被调用(切记m1被调用的意思是 返回了一个增强的dispatch函数然后重写赋值给store.dispatch),m3最后被调用赋值给store.dispatch,那么在真正dispatch一个action的时候(也就是说applyMiddleWare函数被执行完毕,dispatch已经拥有若干中间件),m3自然会最先被调用,所以我们需要把传进来的middleWares数组reverse一下。(自认为这块没说清楚,所以画了张图。当然这个图也不太清楚,但我有的时候会觉得这些说不清楚的东西,一个是讲述能力问题, 另一个真的可能 👉 只可意会不可言传,跑题了....)。

现在你终于有了applyMiddleWare了,执行它来增强dispatch:

1
applyMiddleWare(store, [logger, catchError]);

你还是觉得不够,于是你仔细观察发现,每次中间件函数里面都有一句:

1
const next = store.dispatch;

哦!这是在暂存最新的dispatch,然后利用闭包引用到它!好像函数的参数也是一个闭包,那为何不直接把这个next当做参数传进去呢!:

1
2
3
4
5
6
const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

由于我们改写了我们的中间件,那么applyMiddleWare也要进行修改:

1
2
3
4
5
6
7
8
9
function applyMiddleware(store, middlewares) {
  middlewares = middlewares.slice();
  middlewares.reverse();
  let dispatch = store.dispatch;
  middlewares.forEach(middleware => {
    dispatch = middleware(store)(dispatch)
  })
  return Object.assign({}, store, {dispatch})
}

真正的applyMiddleWare长什么样子呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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)))
}


export default function applyMiddleware(...middlewares) {
  return (createStore) => (...args) => {
    const store = createStore(...args)
    let dispatch = store.dispatch
    let chain = []
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }

    chain = middlewares.map(middleware => middleware(middlewareAPI))

    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

上面的代码有几处真的可以拿出来说一说,第一个就是compose函数,它的作用就是接收N个函数,然后从右到左调用这些函数且将上一次调用的结果传入下一次调用的函数里,还记得上面我们大费周章解释那个数组reverse吗?这个compose神奇的地方就是我们不需要再reverse,它本身返回的就是一个倒序的函数数组调用!

const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
}

还有一处值得说的是上面的dispatch为什么要用匿名函数包一层呢?其实是因为在我们的中间件里,有的时候需要获取到最终的一个dispatch!所以我们通过闭包来引用到dispatch这个函数,dispatch后续被不断赋值,我们一直引用着它。可是为什么我们要用到一个最终的dispatch呢,我们可以参考redux-thunk的源码来解释:

1
2
3
4
5
6
7
8
9
function createThunkMiddleware(extraArgument) {
 return ({dispatch, getState}) => next => action => {
   if(typeof action === 'function') {
      return action(dispatch, getState, extraArgument)
   }
   return next(action)
 }

}

redux-thunk解决的是异步API调用的问题,它监测我们的action是否为函数,如果是函数,则将dispatchgetState传给它,它处理完异步再重新dispatch。重新dispatch的时候就需要用到最终的dispatch(也就解释了为什么我们需要一个能够捕获最终dispatch的闭包)。我们发送一个action,且这个action是函数时,我们的终点就是要走到redux-thunk这个中间件,走到这就够了。然后后续异步API调用成功或者失败,我再重新dispatch一个action而这个action就是一个对象,这个action将再一次历经所有的中间件,直至最原始的dispatch,它的使命也终将结束....

写到最后我竟然有一点感动,我好像代入了action这个角色,它从起点出发,带着它的使命,历经一个个中间件,最终到底reducer,改变应用状态,这就是它的全部使命,我们又何尝不是带着各种使命来到这个美好又残酷的世界,一步步走向终点呢....

updatedupdated2020-06-212020-06-21