最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 写给自己看的React源码解析(五):前端必学-Redux中间件的实现原理

    正文概述 掘金(zouwowo)   2021-01-30   436

    前言

    中间件的内容其实不属于React源码相关,属于Redux相关。但是中间件的原理是一个非常重要的知识点,它是我们前端开发解决一些业务问题时的利器。很多前端应用的架构都是使用中间件为基础搭建的。

    本文不会介绍Redux相关的内容,只关注于中间件的实现原理。

    本文将是我学习react源码的目前阶段的最后一篇文章,react源码内容比较多,也比较晦涩,我也不能够一蹴而就。等过段时间再继续深入学习的时候,再来接着更新这一系列的内容。

    认识Redux中间件

    React开发中,管理数据状态的Redux是每个人都会接触到的内容(如同VuexVue开发当中的地位)。我们知道,在Redux中,我们想要修改数据,我们必须先派发一个ActionAction会被Reducer读取,Reducer将根据Action内容的不同执行不同的计算逻辑,最终生成新的state,这个新的state会更新到Store对象里,进而驱动视图层面作出对应的改变。

    这里有一个需要注意的地方,Redux源码中只有同步操作,也就是说当我们dispatch action时,state会被立即更新。

    如果我们想引入异步数据流,该怎么办?官方的建议就是使用中间件。

    本文使用redux-thunk作为处理异步数据流的方式。源码地址

    import thunkMiddleware from 'redux-thunk'
    import reducer from './reducers'
    // 使用redux-thunk中间件
    const store = createStore(reducer, applyMiddleware(thunkMiddleware))
    

    这样配置之后,我们就可以给dispatch(这个dispatch并非原始的dispatch)传入一个函数,并可以在该函数中使用异步数据流。

    thunk中间件

    thunk的源码很简单,也就10多行代码

    function createThunkMiddleware(extraArgument) {
      // 返回值是一个 thunk,它是一个函数
      return ({ dispatch, getState }) => (next) => (action) => {
        // thunk 若感知到 action 是一个函数,就会执行 action
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }
        // 若 action 不是一个函数,则不处理,直接放过
        return next(action);
      };
    }
    const thunk = createThunkMiddleware();
    thunk.withExtraArgument = createThunkMiddleware;
    export default thunk;
    

    在这里先简单的分析一下它的源码。首先这个方法返回是一个三重的函数,第一重接收的是一个dispatch(组合中间件出来的一个链)和getStatestore.getState获取state数据的方法)的对象,第二重是nextstore.dispatch最原始的dispatch方法),第三重是action(这个方法是一个action对象或者是一个异步的函数)。

    这里的内容可能一下子无法理解,没关系,接下来我们配合ReduxapplyMiddleware的方法流程,一步步来剖析Redux的中间件原理。

    Redux的中间件原理

    我们先看看applyMiddleware的源码。

    // applyMiddlerware 会使用“...”运算符将入参收敛为一个数组
    export default function applyMiddleware(...middlewares) {
      // 它返回的是一个接收 createStore 为入参的函数
      return createStore => (...args) => {
        // 首先调用 createStore,创建一个 store
        const store = createStore(...args)
        let dispatch = () => {
          throw new Error(
            `Dispatching while constructing your middleware is not allowed. ` +
              `Other middleware would not be applied to this dispatch.`
          )
        }
    
        // middlewareAPI 是中间件的入参
        const middlewareAPI = {
          getState: store.getState,
          dispatch: (...args) => dispatch(...args)
        }
        // 遍历中间件数组,调用每个中间件,并且传入 middlewareAPI 作为入参,得到目标函数数组 chain
        const chain = middlewares.map(middleware => middleware(middlewareAPI))
        // 改写原有的 dispatch:将 chain 中的函数按照顺序“组合”起来,调用最终组合出来的函数,传入 dispatch 作为入参
        dispatch = compose(...chain)(store.dispatch)
    
        // 返回一个新的 store 对象,这个 store 对象的 dispatch 已经被改写过了
        return {
          ...store,
          dispatch
        }
      }
    }
    

    因为applyMiddleware是在createStore当中使用的,所以我们也需要看一部分的createStore源码。

    function createStore(reducer, preloadedState, enhancer) {
        // 这里处理的是没有设定初始状态的情况,也就是第一个参数和第二个参数都传 function 的情况
        if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
            // 此时第二个参数会被认为是 enhancer(中间件)
            enhancer = preloadedState;
            preloadedState = undefined;
        }
        // 当 enhancer 不为空时,便会将原来的 createStore 作为参数传入到 enhancer 中
        if (typeof enhancer !== 'undefined') {
            return enhancer(createStore)(reducer, preloadedState);
        }
        ......
    }
    

    createStore对于applyMiddleware的使用逻辑比较简单其实主要就是一句话

    return enhancer(createStore)(reducer, preloadedState);
    

    我们把这几个参数,代入applyMiddleware中再去看。

    // middlewares是传入的中间件
    export default function applyMiddleware(...middlewares) {
      // 它返回的是一个接收 createStore 为入参的函数
      return createStore => (...args) => {
        ......
      }
    }
    

    applyMiddleware方法中的createStore,其实就是Redux中的createStore方法,而args则对应的是reducerpreloadedState,这两个参数均为createStore函数的约定入参。

    我们接着来看下面的内容

    // 首先调用 createStore,创建一个 store
    const store = createStore(...args)
    // 用来防止在遍历 middleware 时调用dispatch
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }
    
    // middlewareAPI 是中间件的入参
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 遍历中间件数组,调用每个中间件,并且传入 middlewareAPI 作为入参,得到目标函数数组 chain
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    

    这里的内容就是调用createStore创建一个store。然后创建一个middlewareAPI对象,遍历middleware(中间件),并传入middlewareAPI参数。这里其实就是之前thunk第一重函数接收的dispatchgetState

    然后看下一句

    dispatch = compose(...chain)(store.dispatch)
    

    这里的compose(...chain)我们下面的章节再看,这里可以先把这个方法看成如下

    dispatch = middleware(middlewareAPI)(store.dispatch)
    

    这里正好对于thunk的第二重函数接收next参数。这里要注意一点,dispatch已经被重写。

    return {
      ...store,
      dispatch
    }
    

    最后返回一个重写过dispatch的对象,这个对象会被绑定到我们的页面上,比如

    <Provider store={store}>
      <App />
    </Provider>
    

    然后我们通过useDispatch拿到的dispatch其实就是thunk中间件改写过的方法。我们用这个dispatch去传入action对象或者异步数据流函数,其实就是调用thunk的第三重函数

    (action) => {
      // thunk 若感知到 action 是一个函数,就会执行 action
      if (typeof action === 'function') {
        return action(dispatch, getState, extraArgument);
      }
      // 若 action 不是一个函数,则直接使用dispatch派发action
      return next(action);
    };
    

    注意一点,这里的dispatch已经是被改写的dispatch = compose(...chain)(store.dispatch)

    这样,我们就通过中间件实现了Redux的异步数据流。我们就可以给dispatch传入一个函数,来异步的派发action

    compose

    compose是一个函数式编程中,很常用的工具方法。它的功能就是把多个函数组合起来。

    redux中若有多个中间件,那么redux会结合它们被“安装”的先后顺序,依序调用这些中间件。所以,我们需要使用compose方法把中间件函数组合起来。

    注意:compose只是一个函数式编程的思路,实现方式有很多种,下面只是redux中实现的一种

    port default function compose(...funcs) {
      // 处理数组为空的边界情况
      if (funcs.length === 0) {
        return arg => arg
      }
    
      // 若只有一个函数,也就谈不上组合,直接返回
      if (funcs.length === 1) {
        return funcs[0]
      }
      // 若有多个函数,那么调用 reduce 方法来实现函数的组合
      return funcs.reduce((a, b) => (...args) => a(b(...args)))
    }
    
    compose(f1, f2, f3, f4);
    // => 会转换成下面这种形式
    (...args) =>  f1(f2(f3(f4(...args))));
    

    多个中间件嵌套的时候,这里的思路可能会有点绕,我举个例子来说明一下。假设我们使用了两个中间件,一个是thunk,一个是redux-logger(当redux数据变更的时候,会打印相关信息到控制台)。源码地址

    // dispatch定义
    dispatch = thunk(createLogger(store.dispatch));
    // dispatch被使用
    dispatch(action);
    //等同于
    thunk(createLogger(store.dispatch))(action)
    

    我们来看下redux-logger的源码,如果我只保留与redux中间件相关的逻辑的话,它的源码可以压缩成几行代码。

    return ({ getState }) => next => (action) => {
      return next(action);
    };
    

    这里也有三重的代码,第一重在我们遍历中间件数组的时候就被调用了

    const chain = middlewares.map(middleware => middleware(middlewareAPI));
    

    所以这里传入thunk的应该是第二重的代码

    thunk(createLogger(store.dispatch))(action)
    // 等同于
    thunk((action) => store.dispatch(action))(action)
    

    我们再来看看之前thunk中间件的源码

    function createThunkMiddleware(extraArgument) {
      // 返回值是一个 thunk,它是一个函数
      return ({ dispatch, getState }) => (next) => (action) => {
        // thunk 若感知到 action 是一个函数,就会执行 action
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }
        // 若 action 不是一个函数,则不处理,直接放过
        return next(action);
      };
    }
    const thunk = createThunkMiddleware();
    thunk.withExtraArgument = createThunkMiddleware;
    export default thunk;
    

    我们再次来精简代码

    thunk((action) => store.dispatch(action))(action)
    // 等同于
    const thunk = (next) => (action) => {
      if (typeof action === 'function') {
        return action(dispatch, getState, extraArgument);
      }
      return next(action);
    };
    thunk((action) => store.dispatch(action))(action);
    // 等同于
    const thunk = (action) => {
      if (typeof action === 'function') {
        return action(dispatch, getState, extraArgument);
      }
      return store.dispatch(action);
    };
    thunk(action);
    

    这样,就实现了多个中间件的依次调用。

    感谢

    如果本文对你有所帮助,请帮忙点个赞,感谢!


    下载网 » 写给自己看的React源码解析(五):前端必学-Redux中间件的实现原理

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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