what
why
我们为什么要用中间件? 提出一个需求: 每个请求都输出日志,或进行打点,如果没有一个统一的中间件进行操作,需要手动 console.log()
再比如需要执行一个异步请求;
where
redux是有流程的,那么,我们该把这个异步操作放在哪个环节比较合适呢?
- Reducer纯函数只承担计算State功能,不适合其它功能。
- View?与State一一对应,可以看做是State的视觉层,也不适合承担其它功能。
- Action?它是一个对象,即存储动作的载体,只能被操作。
其实,也只有dispatch能进行额外的扩展。那么怎么在dispatch中添加其它操作呢?
how
import { createStore, applyMiddleware } from 'redux'
/** 定义初始 state**/
const initState = {
score : 0.5
}
/** 定义 reducer**/
const reducer = (state, action) => {
switch (action.type) {
case 'CHANGE_SCORE':
return { ...state, score:action.score }
default:
break
}
}
/** 定义中间件 **/
const logger = ({ getState, dispatch }) => next => action => {
console.log('日志:即将执行:', action)
// 调用 middleware 链中下一个 middleware 的 dispatch。
let returnValue = next(action)
console.log('日志:执行完成后 state:', getState())
return returnValue
}
/** 创建 store**/
let store = createStore(reducer, initState, applyMiddleware(logger))
/** 现在尝试发送一个 action**/
store.dispatch({
type: 'CHANGE_SCORE',
score: 0.8
})
/** 打印:**/
// 日志:即将执行: { type: 'CHANGE_SCORE', score: 0.8 }
// 日志:执行完成后 state: { score: 0.8 }
解读
createStore函数接收参数为(reducer, [preloadedState], enhancer),其中preloadedState为初始state,那么 enhancer 又是什么呢?从官方文档可以看到,StoreCreator 的函数签名为
type StoreCreator = (reducer: Reducer, initialState: ?State) => Store复制代码
是一个普通的创建 store 的函数,而 enhancer 的签名为
type enhancer = (next: StoreCreator) => StoreCreator复制代码
可知enhancer是一个组合 StoreCreator 的高阶函数, 返回的是一个新的强化过的 StoreCreator,再执行StoreCreator就能得到一个加强版的 store。
在本例里形参enhancer即为applyMiddleware,从下面的源码可知,applyMiddleware 改写了 store 的 dispatch 方法,新的 dispatch 即是被所传入的中间件包装过的。
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
// 接收 createStore 参数
var store = createStore(reducer, preloadedState, enhancer)
var dispatch = store.dispatch
var chain = []
// 传递给中间件的参数
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
// 注册中间件调用链,并由此可知,所有的中间件最外层函数接收的参数都是{getState,dispatch}
chain = middlewares.map(middleware => middleware(middlewareAPI))
//compose 函数起到代码组合的作用:compose(f, g, h)(...args) 效果等同于 f(g(h(...args))),具体实现可参见附录。从此也可见:所有的中间件最二层函数接收的参数为 dispatch,一般我们在定义中间件时这个形参不叫 dispatch 而叫 next,是由于此时的 dispatch 不一定是原始 store.dispatch,有可能是被包装过的新的 dispatch。
dispatch = compose(...chain)(store.dispatch)
// 返回经 middlewares 增强后的 createStore
return {
...store,
dispatch
}
}
}复制代码
这样下来,原来执行 dispatch(action) 的地方变成了执行新函数
(action)=>{
console.log('日志:即将执行:', action)
dispatch(action)
console.log('日志:执行完成后 state:', getState())
}复制代码
这样就实现了action -> reducer的拦截,所以每次触发 action 都能被 log 出来了
compose 函数
在实现compose方法之前我们先考虑一个问题,现在middlewares的结构是这样的,多层嵌套,一个函数嵌入一个函数,我们改如何将这个方法从嵌套中解放出来呢?
function A(){
function B(){
function C(){
}
}
}
复制代码
如何能避免面多层的嵌套?通过把函数赋值给一个参数,可以解放嵌套,但这样不太现实,因为我们需要创建许多的参数。
const CM=function C(){}
const BM=function B(){
CM()
}
const AM=function A(){
BM()
}
复制代码
为了避免创建许多不必要的引用,我们可以用传递参数的方式来解决这个问题,直接将函数当作参数传入,那么就要注意一个问题,因为我们要先传入函数,但是不执行各函数,所以每个函数我们都要返回一个函数,也就是创建高阶函数,等都准备好了,从最外层的函数开始调用执行。
function C(){
return function(){}
}
function B(CM){
return function(){
CM()
}
}
function A(BM){
return function(){
BM()
}
}
复制代码
这个方法执行的方式就很恶心,是一个函数嵌套后面的一个函数,将C返回的函数传入B,然后将B返回的函数传入A,最后执行()
,逐层执行函数,这样也就没有逃离回调地狱。
let compose=A(B(C()))
compose()
通过 reduce 函数解决
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
const last = funcs[funcs.length - 1]
const rest = funcs.slice(0, -1)
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}
精妙之处就在巧妙的利用了 Array.prototype.reduceRight(callback[, initialValue]) 这个我们平时不怎么用到的函数。该方法将数组中每一项从右向左调用callback,本例中的callback即为
middleware 的设计有点特殊,是一个层层包裹的匿名函数,这其实是函数式编程中的柯里化 curry,一种使用匿名单参数函数来实现多参数函数的方法。applyMiddleware 会对 logger 这个 middleware 进行层层调用,动态地对 store 和 next 参数赋值。
柯里化的 middleware 结构好处在于:
- 易串联,柯里化函数具有延迟执行的特性,通过不断柯里化形成的 middleware 可以累积参数,配合组合( compose,函数式编程的概念,Step. 2 中会介绍)的方式,很容易形成 pipeline 来处理数据流。
- 共享store,在 applyMiddleware 执行过程中,store 还是旧的,但是因为闭包的存在,applyMiddleware 完成后,所有的 middlewares 内部拿到的 store 是最新且相同的。
总结
redux 中间件通过改写 store.dispatch 方法实现了action -> reducer的拦截,从上面的描述中可以更加清晰地理解 redux 中间件的洋葱圈模型:
中间件A -> 中间件B-> 中间件C-> 原始 dispatch -> 中间件C -> 中间件B -> 中间件A
常用中间件
redux-thunk
对于异步中间件的情况也同理 , 以 redux-thunk
为例:
// 这是简化后的 redux-thunk
const thunk = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};复制代码
这里可以看到,当 dispatch
的收到的 action
为函数时,将试图嵌套执行这个函数。套用这个中间件后的 dispatch
方法就更 “聪明” 了,这就是为什么 redux
中规定 action
必须为纯对象而在 redux-thunk
中传的 action
却是 function
而不会报错的原因。