写点什么

给我五分钟!让你掌握 React 的 Redux 中间件原理

  • 2023-07-14
    北京
  • 本文字数:5370 字

    阅读完需:约 18 分钟

给我五分钟!让你掌握React的Redux中间件原理

前言

Redux 是一个基于 Flux 架构的 JavaScript 应用状态管理库,提供可预测性的状态管理方案。其中,middleware 更是 Redux 中一个重要的概念,它存在使得 Redux 应用更加灵活、可扩展、可维护。本文中,我们将探讨 Redux middleware 的运行机制和实现原理,最后带您轻松实现一个自己的 middleware。无论你是初学者还是有一定经验的开发者,相信本文都能给你带来一些新的启示和技巧。让我们一起探索 Redux middleware 的魅力吧!

什么是 Middleware

Redux middleware 是一种可插拔的机制,用于在 Redux 的 dispatch 函数被调用后, reducer 处理 action 之前,对 action 进行拦截、变换、增强等操作。Redux middleware 可以用于很多场景,例如:


  • 异步操作:Redux 本身是同步的,但是我们可以使用 middleware 来处理异步操作,例如发起网络请求,等待数据返回后再更新 store;

  • 日志:用于记录每个 action 的执行过程,以便于调试和分析;

  • 认证和授权:可以拦截所有 action,然后进行认证和授权,以确保只有授权用户可以执行某些操作。


middleware 简化后的核心逻辑如下:


    const middleware = store => next => action => {      // do something before dispatching the action      const result = next(action);      // do something after dispatching the action      return result;    };
复制代码


通过以上代码可以看出 middleware 本质上就是一个接受 store、next、action 三个参数的函数。其中,store 是 Redux 的 store 对象,next 是 dispatch 函数,action 是当前的 action 对象。


使用 Middleware

在 Redux 中使用 middleware 非常简单,只需要在创建 store 的时候使用 applyMiddleware 函数将 middleware 应用到 store 上即可,例如:


    import { createStore, applyMiddleware } from 'redux'    import rootReducer from './reducers'    import middleware1 from './middleware/middleware1'    import middleware2 from './middleware/middleware2'    const store = createStore(      rootReducer,      applyMiddleware(middleware1, middleware2)    )
复制代码


在上面的代码中,我们使用了 applyMiddleware 函数将 middleware1,middleware2 应用到 store 上。这样,当我们调用 store.dispatch(action) 时,middleware 就会被依次执行,直到 reducer 处理 action。

Middleware 内部运行机制及原理剖析

我们通过上文的使用方式发现,middleware 是通过 createStore 来增强和扩展原来的 dispatch。下面我们就从 createStore 入手,逐步对 middleware 进行剖析:


  • createStore 源码分析


    //简化后的源码      import { Action } from './types/actions'      import { Reducer } from './types/reducers'      export function createStore<        S,        A extends Action,        Ext extends {} = {},        StateExt extends {} = {}      >(        reducer: Reducer<S, A>,        preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,        enhancer?: StoreEnhancer<Ext, StateExt>      ): Store<S, A, StateExt> & Ext {        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<S, A, StateExt> & Ext        }        let currentReducer = reducer        let currentState = preloadedState as S        let currentListeners: Map<number, ListenerCallback> | null = new Map()        let nextListeners = currentListeners        let listenerIdCounter = 0        let isDispatching = false        function ensureCanMutateNextListeners() {          if (nextListeners === currentListeners) {            nextListeners = new Map()            currentListeners.forEach((listener, key) => {              nextListeners.set(key, listener)            })          }        }        function getState(): S {          ...        }        function subscribe(listener: () => void) {           ...          let isSubscribed = true            ensureCanMutateNextListeners()          const listenerId = listenerIdCounter++          nextListeners.set(listenerId, listener)          return function unsubscribe() {            if (!isSubscribed) {              return            }            if (isDispatching) {              throw new Error('...')            }            isSubscribed = false            ensureCanMutateNextListeners()            nextListeners.delete(listenerId)            currentListeners = null          }        }        function dispatch(action: A) {          ...        }        dispatch({ type: ActionTypes.INIT } as A)        const store = {          dispatch: dispatch as Dispatch<A>,          subscribe,          getState        } as unknown as Store<S, A, StateExt> & Ext        return store      }
复制代码


从以上代码,createStore 方法接收三个参数:reducer、preloadedState 和 enhancer。如果传入了 enhancer 则使用 enhancer 来增强 store(实际上是通过重写 createStore 来增强 dispatch),否则就返回一个包含 getState、dispatch 和 subscribe 方法的 store 对象。其中,这里的第三个参数 enhancer 就是我们下文要分析的 applyMiddleWare。


  • applyMiddleware 源码分析


    //简化后的源码    export default function applyMiddleware(      ...middlewares: Middleware[]    ): StoreEnhancer<any> {      return createStore =>        <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          }        }    }
复制代码


如上所示,先通过轮询执行 middleware 柯里化函数第一层来为每个 middleware 函数提供 getState 和 dispatch;再通过 compose 将所有 middleware 串联起来形成一个函数链,从而实现对 Redux 数据的拦截和处理,并最终返回一个增强版的 dispatch。我们看到在 applyMiddleWare 中 compose 是核心逻辑,下面我们具体分析下 compose 是如何进行 middleware 函数聚合的。



    • compose 源码分析



    export default function compose(...funcs: Function[]) { if (funcs.length === 0) { // infer the argument type so it is usable in inference down the line return <T>(arg: T) => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args) => a(b(...args))); }
    复制代码


    如上所示,这段代码首先判断 funcs 数组的长度,如果长度为 0,则直接返回一个函数;如果长度为 1,则直接返回 funcs[0];如果长度大于 1,则使用 reduce 方法通过把后一个的 middleware 的结果当成参数传递给下一个 middleware 的方式将 funcs 数组中的函数依次组合起来。这里的 func 也就是接收 next 即 dispatch 作为参数的 middleware 柯里化函数第二层,func 执行后会返回一个新函数 action => next(action)。最终 compose 返回一个新函数,并按照从右到左的顺序依次调用每个 func 进行处理,这个函数就是增强版的 dispatch。


    接下来,我们可以用“把大象放冰箱”这个哲理题作为一个示例,来继续加深对 compose 函数的理解:


        function putElephantInFridge(){      console.log('打开冰箱门');      console.log('把大象放进去');      console.log('关上冰箱门');    }
    复制代码


    这个函数实现起来虽然简单,但不好进行继续扩展。为了便于扩展我们把这个大函数拆解并抽象化,让每个函数都是独立的,只负责完成自己的任务,最后再实现一个通用函数来获取最后的结果:


        function openFridgeDoor() {      console.log('打开冰箱门');    }    function putSomethingInFridge(something) {      console.log(`把${something}放进去`);    }    function closeFridgeDoor() {      console.log('关上冰箱门');    }    const putInFridge = (something)=>compose(closeFridgeDoor,()=>{putSomethingInFridge(something)},openFridgeDoor)();    const putInFridgeAndNotClose = (something)=>compose(()=>{putSomethingInFridge(something)},openFridgeDoor)();    putInFridge('牛奶'); // 打开冰箱门  把牛奶放进去  关上冰箱门    putInFridgeAndNotClose('苹果'); // 打开冰箱门  把苹果放进去  
    复制代码


    在上面的代码中,我们使用 compose 函数将三个单独的函数组合成了一个函数 putInFridge,该函数接收一个参数 something,并依次执行三个步骤,最终将 something 放进了冰箱中。另外,我们也可以将其中两个函数组合成函数 putInFridgeAndNotClose。由上我们看到,compose 函数是非常实用的一个函数,通过它可以将任意多个函数组合在一起,实现更加灵活和有序的函数调用,增强了程序的复用性、可读性、可测性。

    Middleware 实现方式

    在 Redux 应用中,我们可以使用多种方式来实现 middleware。下面我们将介绍两种主要的实现方法:


    • 基于洋葱模型实现


    用过 express、koa 同学应该都知道它们也都有 middleware 概念,Redux middleware 的实现和 koa 的洋葱模型的机制相似。Redux middleware 在 dispatch action 和到达 reducer 之间提供第三方扩展点,这种实现方式的代码结构类似于洋葱,形成了一层层的包裹,每一层都可以执行一些操作,在每一层中可以对 action 进行处理。



    • 基于装饰器实现


    基于装饰器相对于基于洋葱模型更加直观和易于理解,但是它需要使用 ES7 中的装饰器语法,需要做一定的兼容性处理,这里不做过多阐述。

    编写自定义 Middleware

    基于以上简要剖析,我们接下来可以进行开发属于自己的 middleware。下文是一个最简单的 middleware:


        const loggerMiddleware = storeAPI => next => action => {      console.log('dispatching', action)      let result = next(action)      console.log('next state', storeAPI.getState())      return result    }
    复制代码


    这个简易版的 logger 负责在控制台中打印出当前动作的类型及当状态发生变化时打印出最新的状态。使用它可以帮助开发人员更快地发现应用中的异常。


    • 为什么要使用 storeAPI => next => action =>这种形式呢?


    要回答这个问题我们可以先来看下 Redux 三大原则:1)单一数据源;2)state 是只读;3)使用 reducer 纯函数进行更改。Redux middleware 的 storeAPI 参数包含了整个 Redux store 的状态和 dispatch 方法,这保证了 Redux 应用中只有一个单一的数据源;middleware 中的状态是只读的,不能被直接修改状态;Redux middleware 中的 next 函数它接收一个动作作为参数,并返回一个新的函数。因此,采用这种形式正是更好的遵循 Redux 的设计原则,确保 Redux 应用程序的可预测性、可维护性和可扩展性。另外,在 Redux 社区中也有对使用这种形式的不同声音,他们认为“there is essentially no need to split the arguments into different function calls”、“minor change could promote authors' familiarity and understanding, thus encourage the development of additional middleware to Redux”,关于这块您可以自行扩展阅读。

    总结

    middleware 是 Redux 应用中的一个重要概念,Redux middleware 的原理是基于 Redux 设计原则和函数式编程思想,通过函数柯里化和函数组合来实现对 dispatch 的增强,使得在数据流传递过程中可以插入一些自定义的操作。最后,希望本文能够帮助读者加深对 middleware 原理的理解,助您开发出更加稳定、高效的 react 应用。

    参考文献

    https://redux.js.org/tutorials/fundamentals/part-4-store#middleware


    https://github.com/reduxjs/redux

    用户头像

    前端技术创新 体验优化 分享经验 共同进步 2018-11-25 加入

    通过技术的创新和优化,为用户创造更好的使用体验,并与更多的前端开发者分享我们的经验和成果。我们欢迎对前端开发感兴趣的朋友加入我们的团队,一同探讨技术,共同进步。

    评论

    发布
    暂无评论
    给我五分钟!让你掌握React的Redux中间件原理_汽车之家客户端前端团队_InfoQ写作社区