Redux 中间件 到底怎么工作的呢?

中间件其实不仅仅是Redux专利,在Node框架中也有应用,比如koa,这里我们简单的默认为Redux 中间件,在进入正题前,先了解一下面向切面编程与中间件的关系

一、中间件与面向切面编程(AOP)

面向切面编程(AOP)的存在,解决了我们熟悉的面向对象(OOP)的局限性,可以将其看作是OOP的补充。比如当我们要为某几个类新增一段共同的逻辑,在OOP模式下,即可通过修改它们共同的父类来实现,但这无疑使得公共类越来越臃肿。那如果换成AOP,则可将 扩展逻辑在工作流中的执行节点视为一个单独“切点”,形成一个可以拦截前序逻辑的“切面”。

假设一个通用性很强,业务性很弱的日志追溯需求:要求在每个Action被派发后,打出一个console.log记录这个动作,面向切面编程(AOP)会如何处理?

71f378498bd75ef3undefined

可见,“切面”与业务逻辑是分离的,通过“即插即用”的方式自由的组织自己想要扩展的功能(异步工作流、性能打点、日志追溯等),它是典型的“非侵入式”的逻辑扩展思路,提升了组织逻辑的灵活度与干净度,规避逻辑冗余、逻辑耦合的问题。

二、中间件的引入

通过分析了Redux源码的主流程,我们可以肯定redux源码只有同步操作,也就是当dispatch action 时,state会被立即更新。若需要引入异步数据流,Redux官方则建议使用中间件来增强createStore的能力,它对外暴露了applyMiddleware函数,接受任意个中间件作为入参,返回作为createStore的入参的值

// 引入 redux
import { createStore } from 'redux'
// 创建 store
const store = createStore(
    reducer,
    initial_state,
    applyMiddleware(middleware1, middleware2, ...)
);

三、中间件的机制

  • 我们顺着中间件引入的角度,简单提取一下applyMiddleware源码框架,更加深刻的理解 applyMiddleware 是如何与 createStore 配合工作的?
// 使用“...”运算符将入参收敛为一个数组
function applyMiddleware(...middlewares) {

  // createStore 对应的是 createStore 函数本身,而 args 入参则对应的是 createStore 函数 约定的入参 reducer 和 preloadedState
  return createStore => (...args) => {
  
      // 调用 `createStore`,创建一个 `store`
      const store = createStore(...args)
      // 避免在接下来中间件的串联过程中,dispatch 被调用,即 不允许在构建中间件时进行调度
      let dispatch = () => {
          throw new Error(`Dispatching while constructing your middleware is not allowed. ` + `Other middleware would not be applied to this dispatch.`)
      }
      
      ......下面代码依次放在此处......
      
  }
}
  • dispatch action 时,action 必须是一个普通对象,但使用过中间件的同学会发现 action 是允许为函数的,这背后applyMiddleware 是如何改写dispatch函数的?

1、以 middlewareAPI 作为中间件的入参,逐个调用传入的中间件,获取一个由“内层函数”组成的数组 chain

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    
    const chain = middlewares.map(middleware => middleware(middlewareAPI))

2、调用 compose 函数,将 chain 中的 “内层函数” 逐个组合起来,并调用最终组合出来的函数,传入 dispatch 作为入参

内层函数:在高阶函数中,习惯将原函数称为“外层函数”,将 return 出来的函数称为 “内层函数”

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

3、返回一个新的 store 对象,这个 store 对象的 dispatch 已经被改写过了

    return {
      ...store,
      dispatch
    }
  • 最后,我们深剖一下函数式编程中一个通用的概念,函数合成(compose 函数)
// 利用 ... 运算符将入参收敛为数组格式
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)))
}

reduce 会将数组中的每个元素执行指定的逻辑,并将结果汇总为单个返回值,假设有这样一个 compose 调用

compose(f1,f2,f3,f4)

函数会被组合成这样的形式

(...args) => f1(f2(f3(f4(...args))))

f1,f2,f3,f4这4个中间件的内层逻辑会被组合到一个函数中去,当这个函数被调用时,中间件会依次被调用

四、中间件的工作模式

从中间件的机制中,我们知道 任何的中间件都可以用自己的方式解析dispatch的内容,并继续传递actions 给下一个中间件。但注意:当最后一个中间件开始 dispatch action 时,action 必须是一个普通对象,因为这是同步式的 Redux 数据流 开始的地方。

1、redux-thunk源码解析

我们以为例,探究下中间件的工作模式
接下来,我们透过redux-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
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

redux-thunk源码层面可知道,它主要做的一件事就是 拦截到action后,检查它是否是一个函数

  • 若是函数,则执行它并返回执行的结果
  • 若不是函数,则直接调用next,工作流继续往下走

2、redux-thunk 模拟付款请求

现在,我们假设有这样一个需求:我们需要感知每一次付款请求的发送和响应,并处理请求的结果

  • 引入
import axios from 'axios' 
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';

const store = createStore(
  reducer,
  applyMiddleware(thunk)
);
  • dispatch 一个 action,action 是一个函数
store.dispatch(payMoney(payInfo));
  • payMoney 的返回值是一个函数
// 支付信息
const payInfo = {
  userName: huamu,
  password: xxx,
  count: 1000000
}

const payMoney = (payInfo) => (dispatch) => {
  // 付款前发出准备信号
  dispatch({ type: 'payStart' })

  fetch().then(res => { dispatch()})

  return axios.post('/api/payMoney', {payInfo})
      .then(function (response) {
        console.log(response);
        // 付款成功信号
        dispatch({ type: 'paySuccess' })
      })
      .catch(function (error) {
        console.log(error);
        // 付款失败信号
        dispatch({ type: 'payError' })
      });
}

3、Redux的工作流

b57e1aaf9db9e3d9undefined

结合上面的分析,中间件的工作模式有如下两点可掌握

  • 中间件的执行时机:在action被分发之后、reducer触发之前
  • 中间件的执行前提:applyMiddleware函数对dispatch函数进行改写,使得dispatch触发reducer之前,执行Redux中间件的链式调用。