Skip to content

Redux 源码简读之二 #14

Open
Open
@jappp

Description

@jappp

applyMiddleware

之前解释过 applyMiddleware 是中间件模块,什么是中间件呢?在 Express 框架中,
middleware 是指在框架接受请求到产生响应过程中的代码,可以完成记录日志,内容压缩,添加 CORS headers 等工作,就这类框架而言,其实这一词汇的含义等价于“插件”(plugin)——用于扩展功能的可拆装模块。middleware 最优秀的特性就是可以被链式组合

Redux 的中间件是如何和 Redux 主流程结合的

Redux 中间件将会在 action 被分发之后、到达 reducer 之前执行,并且支持链式调用,按引用顺序依次执行。如下图所示:
image.png

在上一篇 createStore 源码分析中,不难看出一个规律,Redux 源码中只有同步操作,那如果想在 Redux 中引入异步数据流,这时候就要使用 Redux 中间件来增强 createStore了。最经典的异步中间件莫过于 redux-thunk 了 ,以其为例我们可以更好的了解中间件工作原理。

先回顾一下 redux-thunk 的基本使用,如下:

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

// 创建一个有 thunk 中间件加持的 store 对象
const store = createStore(
  reducer,
  applyMiddleware(thunk)
);

// 用于发起付款请求,并处理请求结果。由于涉及资金,我们希望感知请求的发送和响应的返回
// 入参是付款相关的信息(包括用户账密、金额等)
// 注意 payMoney 的返回值仍然是一个函数
const payMoney = (payInfo) => (dispatch) => {
  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' })
  });
}
// 支付信息,入参
const payInfo = {
  userName: xxx,
  password: xxx,
  count: xxx,
  ......
}
// dispatch 一个 action,注意这个 action 是一个函数
store.dispatch(payMoney(payInfo));

再看看 redux-thunk 的源码,非常简洁:

// createThunkMiddleware 用于创建 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;

redux-thunk 做的主要操作就是拦截 action,若 action 是一个函数就会被执行并且返回执行结果,若 action 不是函数就会跳过不做处理。

redux-thunk 带来的改变非常好理解,它允许我们以函数的形式派发一个 action

但是上一篇中我们提过,dispatch 的入参有严格的参数限制,只能是一个对象。为什么启用中间件模块之后会允许以函数的形式派发 action,这就是 applyMiddleware 所做的工作,改写了 dispatch 方法。

Redux 中间件的实现原理

说了这么多,终于到 applyMiddleware 中间件模块了,先来看看源码吧

// applyMiddlerware 会使用“...”运算符将入参收敛为一个数组
export default function applyMiddleware(...middlewares) {
  // 它返回的是一个接收 createStore 为入参的函数
  return createStore => (...args) => {
    // 首先调用 createStore,创建一个 store
    const store = createStore(...args)
    // 一个临时的dispatch,作用是在dispatch改造完成前调用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))
    // 改写原有的 dispatch:将 chain 中的函数按照顺序“组合”起来,调用最终组合出来的函数,传入 dispatch 作为入参
    dispatch = compose(...chain)(store.dispatch)

    // 返回一个新的 store 对象,这个 store 对象的 dispatch 已经被改写过了
    return {
      ...store,
      dispatch
    }
  }
}

我们一步一步来解析源码

1. 首先,applyMiddleware 返回的是一个接收 createStore 为入参的函数,这个函数作为 enhancer 传入 createStore 方法。

回顾一下上一篇的源码

// 当 enhancer 不为空时,便会将原来的 createStore 作为参数传入到 enhancer 中
if (typeof enhancer !== 'undefined') {
    return enhancer(createStore)(reducer, preloadedState);
}

从上面代码可以看出,若 enhancer 存在,那么 createStore 内部就会直接 return 一个针对 enhancer 的调用。在这个调用中,第一层入参是 createStore,对应 createStore 函数本身。第二层入参是 reducer 和 preloadedState,对应 applyMiddleware 中的 args 入参。

enhancer 的意思就是增强器,增强的正是 createStore 的能力,所以传入 createStore 及其入参是很有必要的。

2. 改写 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)

上面的代码片段做了两件事:

  • 以 middlewareAPI 作为入参,逐个调用传入的中间件,获取一个由“内层函数”组成的数组 chain
  • 调用 compose 函数,将 chain 中的“内层函数”逐个组合起来,并调用最终组合出来的函数

按照约定,所有的 Redux 中间件都必须是高阶函数。在高阶函数中,一般习惯把原函数称为外层函数,将 return 出来的函数称为内层函数,以 thunk 源码为例

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

外层函数的作用是获取 dispatch 和 getState 两个 API,真正的中间件逻辑是在内层函数中实现的,等到 middlewares.map(middleware => middleware(middlewareAPI)) 执行完毕,所有的内层函数被提取到 chain 数组。

// 此时chain等于:
chain = [
  next => action => {}, //包含getState、dispatch函数的闭包,假定此函数为f1
  next => action => {},  //包含getState、dispatch函数的闭包,假定此函数为f2
  ...
  next => action => {},  //包含getState、dispatch函数的闭包,假定此函数为fn
]

提取出所有中间件逻辑之后,调用 compose 函数组合所有中间件。然后传入原生 dispatch 执行该函数,返回值就是被改写的 dispatch。这个 compose 函数之前提到过,是一个工具函数用来进行函数合成,并不是 Redux 的专利,我们来看下源码:

// compose 会首先利用“...”运算符将入参收敛为数组格式
export 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 调用

compose([f1, f2, f3, f4])

会把函数组合成

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

如此一来多个中间件的逻辑就会被聚合到一个函数上去,如果被调用时中间件会按顺序依次被执行。

dispatch = compose(...chain)(store.dispatch)
//等价于:
dispatch = f1(f2(...(fn(store.dispatch))))

这就像一个洋葱模型
image

在applyMiddleware方法中,我们传入的「参数」是原始的dispatch方法,dispatch 函数被一层层的改造,返回的「结果」是改造后的dispatch方法。

我们再结合 redux-thunk 的源码去理解,并且小小的优化源码结构,使其看上去更直观易懂,这样dispatch 的改造过程就一目了然了。

function createThunkMiddleware(extraArgument) {
    return function({ dispatch, getState }) { // 这是「中间件函数」
        //参数是store中的dispatch和getState方法
        
        return function(next) { // 这是中间件函数创建的「改造函数」
            //参数next是被当前中间件改造前的dispatch
            //因为在被当前中间件改造之前,可能已经被其他中间件改造过了,所以不妨叫next
            
            return function(action) { // 这是改造函数「改造后的dispatch方法」
                if (typeof action === 'function') {
                  //如果action是一个函数,就调用这个函数,并传入参数给函数使用
                  return action(dispatch, getState, extraArgument);
                }
                
                //否则调用用改造前的dispatch方法
                return next(action);
            }
        } 
    }
}

这差不多就是 Redux 的核心源码了,可以看出格外简洁,但其中的设计思想值得我们深入学习与理解。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions