redux 中间件解码:扩展 redux 功能的秘密

上一篇文章中,介绍了 redux、react-redux 和 redux-toolkit 的核心实现原理,这篇文章我们继续分析 redux 的另一个特性:中间件

redux 中间件是扩展 Redux 功能强大工具,它们充当着 action 被发送到 reducer 之前的拦截器,提供了一种灵活的方式来增强 Redux 的基本功能。接下来我们通过中间件注册和常用中间件实现原理两个方面分析 redux 中间件功能

注册 redux 中间件

在 redux 中,通过 applyMiddleware 方法注册注册中间件,本质是一个 Action 被发送到 Reducer 之前的拦截器,在这段过程之间执行自定义操作,增强 redux 的功能,下面来看具体实现步骤

  • 接收一系列中间件作为参数,每一个中间件是一个高阶函数,遵循 store => next => action => {} 的形式,其中
    • aciton:当前被处理的 action 对象
    • next:调用链中的下一个中间件的 dispatch 函数,用于链式调用
    • store:Store 的引用
  • 初始化传入的中间件,并将中间件组合成一个链式数组
  • 通过 compose 方法替换原有的 dispatch 方法
  • 最后将新的 dispatch 方法返回出去
1export default function applyMiddleware( 2 ...middlewares: Middleware[] 3): StoreEnhancer<any> { 4 // 返回一个函数,这个函数将接收 createStore 函数 5 return (createStore) => (reducer, preloadedState) => { 6 // 使用 createStore 方法和传入的 reducer 和 preloadedState 创建 store 7 const store = createStore(reducer, preloadedState) 8 let dispatch: Dispatch = () => {} 9 10 // 传递给每个中间件的参数 11 const middlewareAPI: MiddlewareAPI = { 12 getState: store.getState, 13 dispatch: (action, ...args) => dispatch(action, ...args), 14 } 15 // 初始化每一个中间件,返回一个中间件链式数组 16 const chain = middlewares.map((middleware) => middleware(middlewareAPI)) 17 // 替换原始的 dispatch 方法 18 dispatch = compose<typeof dispatch>(...chain)(store.dispatch) 19 20 return { 21 ...store, 22 dispatch, 23 } 24 } 25}

applyMiddleware 方法实现中,最重要的部分是如何在 dispatch 之前执行中间件,redux 通过 compose 方法实现这一拦截器的效果

compose 方法通过 reduce 的组合,实际上创建了一个从右到左的函数嵌套,比如有三个中间件函数 [f, g, h, dispatch]compose(f, g, h, dispatch) 执行后会产生 dispatch(f(g(h(...args)))) 的嵌套,从而达到依次执行中间件,再执行具体的 Reducer

1export default function compose(...funcs: Function[]) { 2 // 没有传入函数,直接根据接收到的参数的函数 3 if (funcs.length === 0) { 4 return <T>(arg: T) => arg 5 } 6 7 // 只传入一个函数,直接返回,不用做处理 8 if (funcs.length === 1) { 9 return funcs[0] 10 } 11 12 // 通过 reduce 组合多个函数 13 return funcs.reduce( 14 (a, b) => 15 // 返回一个新的函数,新函数先调用 b,再用 b 的返回值调用 a 函数 16 (...args: any) => 17 a(b(...args)) 18 ) 19}

常用中间件实现原理

在了解了如何在 redux 注册中间件,接下来我们继续分析 redux 常用中间件的实现原理,redux 常用的中间件包括

  • redux-thunk:允许 Action 返回一个函数而不是对象,可以延迟 Action 的派发或者只在特定条件下才派发 Action
  • redux-promise:可以派发一个包含 Promise 的 Action
  • redux-presist:自动保存 Store 到本地存储中(localStorage)

redux-thunk

redux 的实现原理相对简单,就是判断传入的 action 是否是函数

  • 如果是函数,返回 action 函数的直接结果
  • 如果不是函数,直接传递给下一个中间件的 dispatch 函数
1function createThunkMiddleware< 2 State = any, 3 BasicAction extends Action = AnyAction, 4 ExtraThunkArg = undefined, 5>(extraArgument?: ExtraThunkArg) { 6 const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> = 7 ({ dispatch, getState }) => 8 (next) => 9 (action) => { 10 // action 是函数,返回 action 函数的直接结果 11 if (typeof action === 'function') { 12 return action(dispatch, getState, extraArgument) 13 } 14 15 // action 不是函数,直接传递给下一个中间件的 dispatch 函数 16 return next(action) 17 } 18 return middleware 19} 20 21export const thunk = createThunkMiddleware()

redux-promise

redux-promise 的实现原理也相对简单,就是判断传入的 action 是否是 Promise,如果是 Promise 则在 Promise 处理之后(then / catch),将处理结果派发为一个新的 Action

这里提一下代码中的 isFSA 判断, FSA(Flux Standard Action)是一种约定,用于定义 Redux 和类似 Flux 架构中 actions 的格式,目的是为了促进不同的 Redux 应用或库之间的互操作性,具体定义标准如下(就是 redux 的 Action 常用定义)

1{ 2 type: 'ADD_TODO', // 表示 action 的类型,必须 3 payload: {}, // 携带与 action 相关的数据 4} 5

redux-promise 实现代码

1import isPromise from 'is-promise' 2import { isFSA } from 'flux-standard-action' 3 4export default function promiseMiddleware({ dispatch }) { 5 return (next) => (action) => { 6 if (!isFSA(action)) { 7 return isPromise(action) ? action.then(dispatch) : next(action) 8 } 9 10 return isPromise(action.payload) 11 ? action.payload 12 .then((result) => dispatch({ ...action, payload: result })) 13 .catch((error) => { 14 dispatch({ ...action, payload: error, error: true }) 15 return Promise.reject(error) 16 }) 17 : next(action) 18 } 19}

redux-persist

分析 redux-persist 的 4 个 action

  • PERSIST:启动持久化过程,确保从启动时刻起,所有状态更改都会被记录和持久化
  • REHYDRATE:持久化存储中的状态重新注入到 redux Store,应用重新加载或启动时使用
  • PURGE:清除持久化存储中的所有数据
  • PAUSE:暂停持久化过程

redux-persist 的核心实现方法是 persistReducer,这个函数会将传入的 Reducer 改造为一个加强版的 Reducer,加强版的 Reducer 能够在处理 Action 时,将状态更新同步到指定的 Storage(LocalStorage / SessionStorage) 中

我们先看 persistReducer 实现 PERSIST 启动持久化的过程

  • 创建持久化对象:通过 createPersistoid 创建一个新的持久化对象,将状态更新到 Storage 中
  • 获取存储的状态:使用 getStoredState 从 Storage 获取当前持久化的状态
  • 重构(Rehydration):通过 _rehydrate 将 Storage 的状态注入到 redux 中
  • 更新状态:返回一个包含初始持久化状态(_persist)的新状态对象
1export default function persistReducer( 2 config: PersistConfig<S>, 3 baseReducer: Reducer<S, A> 4) { 5 // 获取存储状态,这里的 defaultGetStoredState 就是从 Storage 中获取 Store 数据了 6 const getStoredState = config.getStoredState || defaultGetStoredState 7 // 定义持久化对象,用于更新存储的状态 8 let _persistoid: Persistoid | null = null 9 // 定义是否暂停更新 10 let _paused = true 11 12 return (state: any, action: any) => { 13 const { _persist, ...rest } = state || {} 14 const restState: S = rest 15 16 if (action.type === PERSIST) { 17 // 封闭标志,防止重复重构 18 let _sealed = false 19 const _rehydrate = (payload: any, err?: Error) => { 20 if (!_sealed) { 21 action.rehydrate(config.key, payload, err) 22 _sealed = true 23 } 24 } 25 26 // 解除暂停,开始处理 PERSIST action 27 _paused = false 28 29 // 创建持久化对象,用于后续的持久化操作 30 if (!_persistoid) _persistoid = createPersistoid(config) 31 32 // 注册持久化 key 33 action.register(config.key) 34 35 // 从存储中获取状态 36 getStoredState(config).then((restoredState) => { 37 if (restoredState) { 38 const migrate = config.migrate || ((s, _) => Promise.resolve(s)) 39 migrate(restoredState as any).then((migratedState) => { 40 _rehydrate(migratedState) 41 }) 42 } 43 }) 44 45 // 返回新的状态,并初始化 _persist 46 return { 47 ...baseReducer(restState, action), 48 _persist: { rehydrated: false }, 49 } 50 } 51 52 // 运行基础 reducer 并根据需要更新状态 53 const newState = baseReducer(restState, action) 54 if (newState === restState) return state 55 return conditionalUpdate({ ...newState, _persist }) 56 } 57}

接下来看 persistReducer 在应用重新加载时执行 REHYDRATE 的过程,REHYDRATE 的核心是通过 stateReconciler 确定 Storage 和 redux 状态如何组合,比如哪些部分应该被覆盖、哪些部分应该保留,状态协调器分为有两个版本

  • autoMergeLevel1:默认的状态协调器,在第一层级上合并状态,但不会深入嵌套的对象
  • autoMergeLevel2:在第一层和第二层级上合并状态,对于更深层级的嵌套对象,会保留整个对象

通过 stateReconciler 确定了状态之后,最后调用 conditionalUpdate 方法更新持久化对象

1export default function persistReducer() { 2 const conditionalUpdate = (state: any) => { 3 // 如果状态已重构(rehydrated)且未暂停,则更新持久化对象 4 state._persist.rehydrated && 5 _persistoid && 6 !_paused && 7 _persistoid.update(state) 8 return state 9 } 10 11 // REHYDRATE 实现逻辑 12 if (action.type === REHYDRATE) { 13 // Action key 匹配时才处理 14 if (action.key === config.key) { 15 // 调用基础 reducer 以处理可能存在的其他逻辑 16 const reducedState = baseReducer(restState, action) 17 // Storage 的状态 18 const inboundState = action.payload 19 // 创建状态协调器,合并来自 Storage 的状态(inbound state)与应用当前的初始状态(initial state) 20 const reconciledRest: S = 21 stateReconciler !== false && inboundState !== undefined 22 ? stateReconciler(inboundState, state, reducedState, config) 23 : reducedState 24 25 // 创建新状态,包括重构后的状态和持久化相关的元数据 26 const newState = { 27 ...reconciledRest, 28 _persist: { ..._persist, rehydrated: true }, 29 } 30 // 更新持久化对象 31 return conditionalUpdate(newState) 32 } 33 } 34}