前言

redux 中间件设计的代码,简洁到极致,它的代码量很少,但却满足 redux 设计中间件的要求,柯里化的特性也在这里完美的体现。

涉及的代码

createStore.ts、compose.ts、applyMiddleware.ts

createStore 所有 store 集中所在,这里包括了监听变化,数据存储、替换 reducer、中间件等处理逻辑。

compose 使用数组 reduce 的特性,让数组内的函数从左到右层层向内嵌套,并使用柯里化,把真实的 args 实参传递给最里面的函数。

applyMiddleware 给每一层的中间件函数传递 getState 与改造的 dispatch,而真实的 dispatch 使用 compose 传递给最里面一层的中间件,其他层的 dispatch 都是使用 compose 的第二调用的函数,因此中间件的要使用多层函数而最里层的函数接受 compose 的第三调用的函数的入参,既 action 数据。

compose 实现

在 redux 中 compose 的实现很简洁,但又比较难吃透,因为对数组的 reduce 与闭包结合的理解不够

以下为它的源码:

1
2
3
4
5
6
7
8
9
10
11
function compose(...funcs: Function[]) {
if (funcs.length === 0) {
return <T>(arg: T) => arg;
}

if (funcs.length === 1) {
return funcs[0];
}

return funcs.reduce((a, b) => (...args: any) => a(b(...args)));
}

当一次调用 compose 时,compose 接受多个函数形参,使用扩展扩展运算符获取把所有函数入参以数组的形式获取到,然后当函数个数为 0 时返回一个空数组,当函数个数为 1 时直接返回当前函数,而大于 1 时,使用 reduce 累计函数,让函数之间形成嵌套关系,如

1
2
3
compose(fn1, fn2, fn3);
// === 等价于
fn1(fn2(fn3(...args)));

由于 funcs.reduce 里面使用了箭头函数,理解起来会有点困难,所以转化为以下的:

1
2
3
4
5
funcs.reduce(function (a, b) {
return function (...args: any) {
return a(b(...args));
};
});

而当使用 reduce 累计函数时,如累计 fn1、fn2、fn3,由于 reduce 没有使用默认值,所以第一次遍历的 a、b 分别为 fn1、fn2,而第二次遍历为 reduce 返回的函数与 fn3,累计结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 第一次遍历
a = fn1;
b = fn2;
function (...args: any) {
return fn1(fn2(...args));
};

// 第二次遍历

a = function (...args: any) {
return fn1(fn2(...args));
};
b = fn3;

// 最后累计出来的函数
function (...args: any) {
return (function(...args) {
return fn1(fn2(...args));
})(fn3(...args));
};

当我们调用最后累计出来的函数(既第二次调用 compose)时,就会先调用 fn3、fn2、fn1 这样的顺序去执行,但从源码中是把 store.dispatch 为参数传入

1
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)

而我们每个 fn 都是嵌套函数,如:

1
2
3
4
5
function fn1(next: any) {
return function (targs1: any) {
next(targs1);
};
}

当我们 compose 第二次调用完后返回的 fn1 的内层函数

1
2
3
function(targs1: any){
next(targs1);
};

然后当我们第三次调用 compose 时就会执行 fn1 内层函数,而 next 是 fn2 的内层函数,所以执行顺序为 fn1、fn2、fn3 的内层函数,并把 fn1 的 targs1 一层层传递下去,让每一层对其进行改造或获取处理等然后继续传递下去,直到遇到真正的 dispatch,既 store.dispatch,最后触发 redux 数据更新。

  • 总结:
    compose(funcs)(dispatch)(action),首先会把函数数组改造成嵌套数组,然后第二次调用 dispatch,第三次传入需要修改的数据 action
    执行顺序为

*代表内层函数

1
2
3
fn1(fn2(fn3(dispatch)))

fn3->fn2->fn1->fn1*->fn2*->fn3*
洋葱模型
洋葱模型

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
function applyMiddleware(
...middlewares: Middleware[]
): StoreEnhancer<any> {
return (createStore: StoreCreator) => <S, A extends AnyAction>(
reducer: Reducer<S, A>,
...args: any[]
) => {
const store = createStore(reducer, ...args)
let dispatch: Dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}

const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)

return {
...store,
dispatch
}
}
}

applyMiddleware 是三层,第一次调用传递中间件数组 middlewares,而第二次调用 applyMiddleware 传递 createStore 创建 Store 的,第三次调用传递 reduces 与初始化 State(preloadedState)。

而 applyMiddleware 调用二三次都在createStore.ts

1
2
3
4
return enhancer(createStore)(
reducer,
preloadedState as PreloadedState<S>
) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext

然后中间件需要编写三层函数,第一层是为了传递getState与改造的dispatch的,二三层是为了 compose 实现的,而改造的 dispatch 是如上的fn1*,这样只要不是调用到真正的 dispatch,那么 dispatch 后都会把所有的中间件都走一遍并把数据 action 也传递进去了,那样中间就可以灵活的处理数据。

thunk 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;

thunk 也是先接受改造的 dispatch 与 getState,然后接受 next,从上可知 next 最后一个就是真实的 dispatch,而 thunk 为了实现异步,判断了 action 是否为函数,如果是函数就把改造的 dispatch 与 getState,这样就简单的实现了 redux 只是异步的操作。

而中间件的实现最少要三层函数,因为第一层是给 applyMiddleware 内把改造的 dispatch 与 getState 传入的,而第二三层是给 compose 使用的,因为 compose 调用中间件函数的第一次传入 next(下一次函数)或 dispatch,第二次传入 action 数据,而 action 数据也是 redux 需要的数据,用过 redux 的都知道 reduce 的 action,而这个 action 与这里的是一样的。

从 redux 实现的中间件机制可知,只要我们明确功能是干什么的,核心是什么,那么我们就可以从核心内容在合理的范围内进行实现可预测的扩展机制,从而使功能尽量可面对未来的各种变化而进行扩展适配来符合变化,而 redux 的核心就是变化的数据与触发数据变化的方法,所以我们可以从这方面下手。