最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 理解redux-thunk和redux-promise?从学习redux中间件开始

    正文概述 掘金(村上小树)   2021-07-18   679

    前言

    上一篇文章写了关于redux的作用以及reduxreact-redux两个插件的API,但redux中有一个API:applyMiddleware并没有说明,因为涉及到redux的中间件概念,需要比较多内容去说明,这次这篇文章就集中写一下这方面的知识。

    何为redux中间件

    1. 中间件的作用

    之前我们说过,redux的工作流程图是下面这样的:

    理解redux-thunk和redux-promise?从学习redux中间件开始

    我们看express有中间件机制,其实redux也有,redux的中间件middleware是用来增强dispatch方法的。有时候当我们想改变dispatch执行同时,也执行某些操作,例如日志记录,就可以用中间件实现该需求。如果我们把中间件也纳入到redux的工作流程图,那新的流程图如下所示:

    理解redux-thunk和redux-promise?从学习redux中间件开始

    2. 用到中间件的简单例子

    我们可以拿一个例子来说一下中间件,在上一篇文章中,我们写了一个计数和单位切换的例子,现在拿这个例子再添加一个需求,我希望可以从控制台里知道页面程序调用了哪些action。虽然可以在每个action creator都写打印输出语句,可是这不是最优解,我可以通过插入中间件来达到这个需求:

    目录如下所示:

    理解redux-thunk和redux-promise?从学习redux中间件开始

    新增store/middleware/logger.js文件,内容如下:

    // 中间件用函数来定义
    const logger = store => next => action => {
      console.info('dispatching', action.type)
      next(action)
    }
    
    export default logger
    

    index.js

    import { createStore,applyMiddleware } from 'redux'
    import reducer from './reducer'
    import logger from './middleware/logger'
    
    // createStore的第三个参数是用来定义中间件的,如果initalState(即下面的第二个参数)省略,则可以放在第二个参数的位置传进去
    const store = createStore(reducer,{number:3,unit:'mm'},applyMiddleware(logger))
    export default store
    

    达到的效果如下所示:

    理解redux-thunk和redux-promise?从学习redux中间件开始

    项目代码

    3. 中间件的使用方式

    从上面的例子可知,中间件以函数来定义,其格式为:

    store => next => action => {
        // do something
    }
    

    {}里面需要调用next(action),不然后面的middleware们不会处理该action以及真正触发dispatch(action)

    最后在生成Redux store时作为第二或第三次参数传入到createStore中,传入之前要用applyMiddleware处理。下面来通过分析相关源码来了解下为什么要这么用:

    4. Redux源码中是如何实现中间件的

    createStore

    我们先了解createStore方法:

    createStore(reducer, [preloadedState], enhancer)

    该方法传入2~3个参数,最后会返回一个Redux storeapplyMiddleware(middle)是作为enhancer传入的,enhancer是什么?下面先引用官方的解释对其说明:

    总结以上的引用,其实enhancer是一个用于更改增强Redux store的函数,如何增强?我们先了解下createStore函数的部分代码:

    function createStore(
      reducer,
      preloadedState,
      enhancer
    ) {
        // ...无关代码不展示
        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
            )
        }
        //... 一堆定义store函数的逻辑不展示
        const store = {
        dispatch: dispatch,
        subscribe,
        getState,
        replaceReducer,
        [$$observable]: observable
      } 
      return store
    }
    

    createStore函数的返回结果得知,store本质上是一个带dispatch,subscribe,getState,replaceReducer以及$$observable五个属性的普通object对象。而当调用createStore时有传入enhancer,他会直接返回enhancer(createStore)(reducer,preloadedState),那其实enhancer(createStore)(reducer,preloadedState)执行完成后最终返回的也是一个store,我们可以推断enhancer的编写格式是这样的: (createStore)=>(reducer,preloadedState)=>{return store}。接下来我们看一下生成enhancerapplyMiddleware函数是怎样子的:

    function applyMiddleware(...middlewares){
      return (createStore) =>
        (
          reducer,
          preloadedState
        ) => {
          const store = createStore(reducer, preloadedState)
          // 调用applyMiddleware时不允许middlewares为空
          let dispatch = () => {
            throw new Error(
              'Dispatching while constructing your middleware is not allowed. ' +
                'Other middleware would not be applied to this dispatch.'
            )
          }
    
          const middlewareAPI = {
            getState: store.getState,
            dispatch: (action, ...args) => dispatch(action, ...args)
          }
          /**
           * 通过compose形成调用链
           * compose函数代码:
            function compose(...funcs: Function[]) {
              if (funcs.length === 0) {
                return (arg) => arg
              }
           
              if (funcs.length === 1) {
                return funcs[0]
              }
    
              return funcs.reduce(
                (a, b) =>
                  (...args: any) =>
                    a(b(...args))
              )
            }
           */
          const chain = middlewares.map(middleware => middleware(middlewareAPI))
          dispatch = compose(...chain)(store.dispatch)
          // 通过扩展运算符拆开store后合并成新的对象以更改dispatch方法
          return {
            ...store,
            dispatch
          }
        }
    }
    

    重点说一下这两行代码

    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)
    

    之前说过,中间件的编写格式store => next => action => {// do something},对照上面的代码来分析,假设我们按照以上的编写格式写了两个中间件分别是middleware1middleware2如下所示:

    const middleware1 = store => next => async(action) => {
      console.info('middleware1 start')
      await next(action)
      console.info('middleware1 end')
    }
    
    const middleware2 = store => next => async(action) => {
      console.info('middleware2 start')
      await next(action)
      console.info('middleware2 end')
    }
    

    当调用applyMiddleware(middleware1,middleware2)传入这两个中间件,applyMiddleware内部执行到const chain = middlewares.map(middleware => middleware(middlewareAPI))这一条语句时,middlewareAPI对应编写格式中的store形参,返回的chain是一个数组,其中的元素为 next => action => {// do something} 格式的函数,即是一个描述如何调用dispatch的函数(next是一个经包装或者原始的dispatch,通过next(action)可以派发action)。

    轮到下一条语句dispatch = compose(...chain)(store.dispatch),当执行compose(...chain)时,根据注释中compose函数的源码我们可以推断该语句执行后返回的结果为: (...args: any) =>chain1(chain2(...args)),最终把store.dispatch作为形参传入该函数时,相当于执行chain1(chain2(store.dispatch)),会有下图的执行过程:

    理解redux-thunk和redux-promise?从学习redux中间件开始

    首先执行chain2函数,store.dispatch作为chain2中的next新参传入,chain2立即返回一个格式为action=>{} 的函数,该函数作为chain1中的next新参传入chain1中,而chain1也会返回一个格式一样为action=>{} 的函数赋值给dispatch。该dispatch会在applyMiddleware函数中最后的语句return {...store,dispatch}store合并返回出去。以上过程中,chain1chain2返回的action=>{}的函数都以闭包的方式记录着next变量。

    当在开发代码中dispatch(action)被调用时,会呈现以下的调用流程:

    理解redux-thunk和redux-promise?从学习redux中间件开始

    dispatch指向chain1,故先执行chain1,执行到next(action)语句时,其next指向chain2,故开始执行chain2,执行到next(action)语句时,next指向store原始的dispatch方法,从而实现了增强dispatch方法。上面的调用流程中控制台的输出会是以下的结果:

    middleware1 start
    middleware2 start
    middleware2 end
    middleware1 end
    

    关于异步action

    存在以下需求,我需要把github中的表情包数据放到Redux store中供项目里的多个模块使用,而这些数据需要异步请求获取,这时候我们遇到一个难题,因reducer原则上是纯函数,因此,异步操作这类不纯的行为不能出现在reducer中,针对此问题,我们可以绕个弯子,写个如下的公共函数,获取响应后调用dispatch设置状态,下面我来写一个例子来实践一下上述思路:

    utils\index.js

    import store from '../store'
    import {SET_EMOJIS} from '../store/action'
    
    // 公共函数,用于请求或更新表情图数据
    export function requestEmojis(){
      fetch('https://api.github.com/emojis') // 数据从github的公共开放接口获取
        .then(res=>res.json())
        .then(emojis=>store.dispatch(SET_EMOJIS(emojis)))
    }
    

    下面是store的代码:

    store\index.js

    import { createStore } from 'redux'
    import reducer from './reducer'
    
    // 把数据初始值设为对象
    const store = createStore(reducer,{})
    export default store
    

    store\action\index.js

    // 用于生成设置表情图数据的action的action creator
    export const SET_EMOJIS=(emojis)=>({
      type:'SET_EMOJIS',
      emojis
    })
    

    store\reducer\index.js

    const reducer = (state,action)=>{
      switch (action.type) {
        case 'SET_EMOJIS':
          return action.emojis
        default:
          return state
      }
    }
    
    export default reducer
    

    最后我们来通过以下组件查看效果:

    App.jsx

    import  React  from 'react';
    import { connect } from 'react-redux'
    import {requestEmojis} from '../utils'
    
    const App = (props)=>{
      const {emojis} = props
      return <div>
        <h2>emojis</h2>
        // 点击该按钮后通过调用公共方法requestEmojis获取表情图并存到Redux store中
        <button onClick={requestEmojis}>获取emojis</button><br/>
        {
          Object.entries(emojis)
            .slice(0,50) // 数据有点多,所以只显示50个表情图
            .map(([key,value])=>
              <img src={value} alt={key} title={key} key={key}/>
            )
        }
      </div>
    }
    
    const mapStateToProps = (state) => ({
      emojis:state
    })
    
    export default  connect(mapStateToProps,null)(App)
    

    最后我们来看一下效果:

    理解redux-thunk和redux-promise?从学习redux中间件开始

    项目地址

    但在实际开发中,这种做法并不常用,原因可以等我介绍了redux-thunk的用法后,再拿这两种用法分析对比。

    我们更偏向于利用第三方插件实现异步action异步action指指向异步操作的action。下面我们来依次看一下上面所说到的常用的第三方插件redux-thunkredux-promise

    redux-thunk

    使用方法

    我们在上面的例子引入redux-thunk进行改造,在调用createStore创建Redux store时,就要通过applyMiddleware加载redux-thunk,如下所示:

    store\index.js

    import { createStore,applyMiddleware } from 'redux'
    import reducer from './reducer'
    import thunk from 'redux-thunk'
    
    const store = createStore(reducer,{}, applyMiddleware(thunk))
    export default store
    

    然后我们在 store\action\index.js 中加一个异步action如下所示: (注意此处的action是一个函数,而并非是以往的带type属性的纯对象)

    store\action\index.js

    export const SET_EMOJIS=(emojis)=>({
      type:'SET_EMOJIS',
      emojis
    })
    
    // 此处的异步action为一个高阶函数,返回结果也是一个函数
    // 此处的REQUEST_EMOJIS也是一个Action Creator,所谓Action Creator指创建异步action或同步action的函数
    export const REQUEST_EMOJIS = ()=>dispatch => (
      fetch('https://api.github.com/emojis')
        .then((res)=>res.json())
        .then(emojis => dispatch(SET_EMOJIS(emojis)))
    )
    

    最后更改一下App.jsx

    App.jsx

    import  React  from 'react';
    import { connect } from 'react-redux'
    import {REQUEST_EMOJIS} from '../store/action/index'
    
    const App = (props)=>{
      const {emojis} = props
      return <div>
        <h2>emojis</h2>
        <button onClick={props.requestEmojis}>获取表情图</button>
        <br/>
        {
          Object.entries(emojis).slice(0,50).map(([key,value])=>
            <img src={value} alt={key} title={key} key={key}/>
          )
        }
      </div>
    }
    
    const mapStateToProps = (state) => ({
      emojis:state
    })
    
    const mapDispatchToProps = (dispatch) => ({
      requestEmojis: () => dispatch(REQUEST_EMOJIS()),
    })
    
    export default  connect(mapStateToProps,mapDispatchToProps)(App)
    

    这样子就可以不调用异步请求的公共函数的同时也实现上面的效果,项目地址。

    值得注意的是,被dispatch派发的 异步action 是一个函数,格式是(dispatch, getState, extraArgument)=>{}

    源码分析

    现在来分析一下redux-thunk的源码,源码非常简洁,如下所示:

    function createThunkMiddleware(extraArgument) {
      return ({ dispatch, getState }) => (next) => (action) => {
        // 如果传入的action是一个函数,则代表该action为异步action,则把dispatch, getState, extraArgument作为形参传入该异步action执行
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }
    
        return next(action);
      };
    }
    
    const thunk = createThunkMiddleware();
    thunk.withExtraArgument = createThunkMiddleware;
    
    export default thunk;
    

    上面的代码太精简了,我觉得我都不用解释什么了,不过从源码中我们可以看出一点,在使用redux-thunk时,异步action 必须写成(dispatch, getState, extraArgument)=>{} 格式,然后在执行过程中需要调用dispatch(action)派发。

    拓展:为什么要用redux-thunk(此章节可跳过)

    此章节可能跟文章无关,我是兴趣之余写的,可以直接跳过

    为什么目前大多数用的是redux-thunk而不是像开头的异步公共函数的方式去解决异步操作。我从stackoverflow中的问题how-to-dispatch-a-redux-action-with-a-timeout其中Dan Abramov(Redux作者) 的回答中得出了主要的答案:

    对比于redux-thunk,使用异步公共函数的方式会导致:

    1. 不利于服务端渲染

      答案中是这么写的:

      在Redux关于服务端渲染的链接Redux Server Rendering中,这里 我们可以知道,每一次请求经服务端渲染的页面时,后端都会:

      1. 创建一个新的Redux store,选择性地派发部分action
      2. 然后模板页面可能某些占位符用Redux storestate的数据填充
      3. Redux store获取state,然后在和已渲染的HTML放到响应信息中一并传到客户端。客户端会根据响应的state创建Redux store

      在上面采用异步公共函数的方式方案的例子中,store出现在两个地方,一处是<Provider store={store}>中,一处是 requestEmojis公共函数中,在服务端渲染中如果调用到 requestEmojis,那需要保证两个地方的store是同一个实例。这样子会增加后端代码的复杂度。但是如果使用redux-thunk,那store只出现在<Provider store={store}>中,我们不需要考虑保证单例的问题。

    2. 不利于测试代码的编写

      引用答案中的描述:

      在保证上述所说的单例时,我们会很难编写测试用例,因为对于requestEmojis公共函数的测试中,其调用的store是一个真正的Redux store,其duspatch的调用会影响到页面的显示,因此,我们不能通过jest里bypassing-module-mocks中的jest.mock去取替这个store

      Redux不推荐手写Action Creator,他们更推荐使用@reduxjs/toolkit去生成Action Creator。更详细的资料可参考action-creators--thunks。

    3. 难以区分容器组件和展示组件

      什么是容器组件(container components)展示组件(presentational components),我引用别的文章的一张图来解释:

      理解redux-thunk和redux-promise?从学习redux中间件开始 图片来源:blog.csdn.net/weixin_4604…

    起源地下载网 » 理解redux-thunk和redux-promise?从学习redux中间件开始

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元