一、背景
在了解Redux并实现简易的Redux中,我们自己手写了一个简单的Redux。但目前该版本只支持同步操作,对于异步操作,Action为函数的情况,这个版本并不支持,但实际上Redux原本也不支持异步操作的能力,也不能处理函数,他也要借助中间件的实现。(参考Redux Essentials, Part 5: Async Logic and Data Fetching | Redux)
什么是中间件
Enhancers are powerful because they can override or replace any of the store's methods:
dispatch,getState, andsubscribe.
Middleware类似于中间层的意思,这是个广义的名词。当我们的上层应用和下层应用需要进行数据传递/通信的时候,有时候存在我们需要对请求数据、数据处理、日志记录,甚至自定义等操作。这个时候,这个功能放在上、下层都不合适,这个时候,如果有中间层去进行这个工作,那么就不会影响其他层的逻辑了。
Redux就是用了middleware这个概念,让我们去自定义我们的dispatch,实际上就是加多一层修饰器去包裹store。下图是个例子,应该能大概了解中间件的流程了吧。
二、Redux使用中间件
上面我们简单的讲了一下中间件的概念,也讲了redux中间件的简单思路,那么Redux是提供了什么接口让我们把中间件进行嵌入的呢?
答案就是applyMiddleware(参考Redux Fundamentals, Part 4: Store | Redux)
简单使用
自定义中间件
注意,redux中的中间件是有规范的,我们可以参考Redux Fundamentals, Part 4: Store | Redux,下面我们按照官方文档自定义loggerMiddleware。
const loggerMiddleware = storeAPI => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', storeAPI.getState())
return result
}
使用applyMiddlewares引入
import { createStore, Action, applyMiddleware } from "redux";
import loggerMiddleware from "../middlewares/logMiddleware";
function counterReducer(state = 0, action: Action<'incremented' | 'decremented'>) {
switch (action.type) {
case 'incremented':
return state + 1;
case 'decremented':
return state - 1;
default:
return state;
}
}
const store = createStore(counterReducer, applyMiddleware(loggerMiddleware));
export default store;
效果
我们可以看我们的中间件也生效了。
使用第三方中间件
redux目前也有很多第三方的中间件,这里,我们使用redux-thunk以及redux-logger来实验下。
npm install redux-thunk redux-logger
import { createStore, Action, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import logger from "redux-logger";
function counterReducer(state = 0, action: Action<'incremented' | 'decremented'>) {
switch (action.type) {
case 'incremented':
return state + 1;
case 'decremented':
return state - 1;
default:
return state;
}
}
const store = createStore(counterReducer, applyMiddleware(thunk, logger));
export default store;
效果
上述介绍了redux中如何使用中间件,接下来我们看看他的执行流程。
三、执行流程和源码
执行流程
Middleware form a pipeline around the store's
dispatchmethod.
redux中使用中间件的流程如图所示,有些同学可能已经开始熟悉了,这不是洋葱模型吗?确实是的,在koa中,洋葱模型正是如此,且也有中间件概念。
实际上,中间件会进行一层一层修饰包裹我们的store,当然其中最主要的还是我们的dispatch。
这里我们提一下compose,我们使用componse来实现中间件的执行流程。
function compose(...funcs: Array<Function>): Function {
if(funcs.length === 0) {
return (...args: any[]) => args;
}
if(funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args: any[]) => a(b(...args)));
}
Redux支持中间件的源码
createStore
首先,我们先看src/createStore代码。
export default function createStore<
S,
A extends Action,
Ext = {},
StateExt = never
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext {
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function. See https://redux.js.org/tutorials/fundamentals/part-4-store#creating-a-store-with-enhancers for an example.'
)
}
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error(
`Expected the enhancer to be a function. Instead, received: '${kindOf(
enhancer
)}'`
)
}
return enhancer(createStore)(
reducer,
preloadedState as PreloadedState<S>
) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
}
// ...code
dispatch({ type: ActionTypes.INIT } as A)
const store = {
dispatch: dispatch as Dispatch<A>,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
} as unknown as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
return store
}
这里主要是通过接收enhancer参数,从而加入中间件的功能。当有enhancer函数的时候,会将createStore传入生成一个新的函数,用于接收reducer和preloadedState。实际上就是使用了enhancer对store进行包装。
我们通过阅读源码,以及相应的类型,可以发现enhancer返回的类型
源码中类型
export type StoreEnhancer<Ext = {}, StateExt = never> = (
next: StoreEnhancerStoreCreator<Ext, StateExt>
) => StoreEnhancerStoreCreator<Ext, StateExt>
export type StoreEnhancerStoreCreator<Ext = {}, StateExt = never> = <
S = any,
A extends Action = AnyAction
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>
) => Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
简单的类型表达如下
type Enhancer =
(next: StoreEnhancerStoreCreator) =>
(reducer: Reducer, preloadedState?: PreloadedState) =>
Store
在redux中,为我们提供了applyMiddleware函数,让我们只需要编写对应的Middleware,便可以生成Enhancer。
applyMiddleware
export default function applyMiddleware<Ext, S = any>(
...middlewares: Middleware<any, S, any>[]
): StoreEnhancer<{ dispatch: Ext }>
export default function applyMiddleware(
...middlewares: Middleware[]
): StoreEnhancer<any> {
return (createStore: StoreEnhancerStoreCreator) =>
<S, A extends AnyAction>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>
) => {
const store = createStore(reducer, preloadedState)
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只是通过不断包装dispatch函数,最终返回一个新的store。
1. 最基本的store
我们使用了createStore打造了一个最为基础的store对象,此时并没有任何中间件的修饰,所以这时候的store的api(如dispatch)是最基础的。
export default function applyMiddleware(
...middlewares: Middleware[]
): StoreEnhancer<any> {
return (createStore: StoreEnhancerStoreCreator) =>
<S, A extends AnyAction>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S>
) => {
const store = createStore(reducer, preloadedState)
// ... some code
return {
...store,
dispatch
}
}
}
2. 提供storeApi,给中间件注入能力
前面提到,redux中间件的是有格式规范的,如下
const loggerMiddleware = storeAPI => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', storeAPI.getState())
return result
}
所以,其实我们需要提供的storeApi就是一个store.
注意,这里的middlewareApi中dispatch是为了保证上层的dispatch的引用不丢失。
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
3. 构建middlewares的chain,使用compose函数实现中间件的执行流程。
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
首先,我们看chain是啥
const chain = middlewares.map(middleware => middleware(middlewareAPI))
实际上是一个又一个的数组
[(next)=> action => {...}, (next)=> action => {...}, (next)=> action => {...}, ....]
接着,我们看compose做了啥, 我们先看compose(...chain),相当于前一个中间件的next, 会是后面一个中间件的action函数。但是注意,compose返回的是函数,所以,我们需要将默认的dispatch作为参数传入,就有了。从而实现中间件的执行顺序。
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
四、 applyMiddlewares的实现
核心任务只要是实现函数序列执行(实质上是对dispatch函数的包装)。
思路
核心在于middlewareChain的实现,使用compose让上一个中间件执行后的结构给到下一个中间价。以下的代码实现了函数序列执行的逻辑。
export function applyMiddleware<S extends any>(...middleWares: Middleware<any>[]) {
return (createStore: typeof CreateStore) => (reducer: Reducer<S, AnyAction>): Store<S, AnyAction> => {
const store = createStore<S, AnyAction>(reducer);
let dispatch = store.dispatch;
const midApi: Store<S, AnyAction> = {
...store,
dispatch: (action: AnyAction, ...args: any[]) => dispatch(action, ...args)
};
const middlewareChain = middleWares.map(middleware => middleware(midApi));
dispatch = compose(...middlewareChain)(store.dispatch);
return {
...store,
dispatch };
}
}
整体代码实现
import { Reducer, Store, Dispatch, Action, AnyAction, Enhancer } from './typings';
export function createStore<S, A extends AnyAction>(reducer: Reducer<S, A>, enhancer?: Enhancer): Store<S, A> {
if(enhancer) {
return enhancer(createStore as any)(reducer);
}
let currentState: S | undefined;
let currentListeners: Array<() => void> = [];
const getState = () => currentState;
const subscribe: Store<S, A>['subscribe'] = (fn) => {
currentListeners.push(fn);
return () => {
const index = currentListeners.findIndex(fn);
currentListeners.splice(index, 1);
}
}
const dispatch: Dispatch<A> = (action) => {
currentState = reducer(currentState, action);
currentListeners.forEach(listener => listener());
}
dispatch({ type: 'SystemInit' } as A);
return {
getState,
subscribe,
dispatch,
}
}
import { Store, Middleware, Reducer } from './typings';
import { AnyAction } from 'redux';
import { createStore as CreateStore } from './index';
export function applyMiddleware<S extends any>(...middleWares: Middleware<S>[]) {
return (createStore: typeof CreateStore) => (reducer: Reducer<S, AnyAction>): Store<S, AnyAction> => {
const store = createStore<S, AnyAction>(reducer);
let dispatch = store.dispatch;
const midApi: Store<S, AnyAction> = {
...store,
dispatch: (action: AnyAction, ...args: any[]) => dispatch(action, ...args)
};
const middlewareChain = middleWares.map(middleware => middleware(midApi));
dispatch = compose(...middlewareChain)(store.dispatch);
return {
...store,
dispatch };
}
}
function compose(...funcs: Array<Function>): Function {
if(funcs.length === 0) {
return (...args: any[]) => args;
}
if(funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args: any[]) => a(b(...args)));
}
效果
我们使用两个中间件试一下。由于并没有使用redux中的类型定义,导致applyMiddleware使用了两个第三方的middleware类型会不同,所以这里先用any进行hack处理。
import { Action } from './../lib/redux-nut/typings';
// import { createStore, Action, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import logger from "redux-logger";
import { createStore } from '../lib/redux-nut';
import { applyMiddleware } from '../lib/redux-nut/applyMiddleware';
function counterReducer(state = 0, action: Action<'incremented' | 'decremented'>) {
switch (action.type) {
case 'incremented':
return state + 1;
case 'decremented':
return state - 1;
default:
return state;
}
}
const store = createStore(counterReducer, applyMiddleware(thunk as any, logger as any));
export default store;