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

1,014 阅读7分钟

前言

中间件的内容其实不属于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);

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

感谢

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