从使用到原理,手撸一个自己的redux中间件

2,196 阅读6分钟

中间件是个什么东西?

redux中管理数据的流程是单向的,就是说,从派发动作一直到发布订阅触发渲染是一条路走到头,那么如果想要在中间添加或是更改某个逻辑就需要找到action或是reducer来修改,有没有更方便的做法呢?

redux的流程:

button -触发事件-> dispath -派发动作-> reducer -发布订阅-> view

而中间件(middleware)就是一个可插拔的机制,如果想找扩展某个功能,比如添加日志,在更新前后打印出state状态,只需要将日志中间件装到redux上即可,于是便有了日志功能,当不想使用时可再拿掉,非常方便。

中间件的使用

先说说用法,只有会用了,再说原理。

redux-logger

redux提供了好多个现成的中间件,比如上面提到的日志中间件,安装它即可使用:

npm i --save redux-logger

redux包提供了一个方法可以装载中间件:applyMiddleware

在创建store对象的时候,可以传入第二个参数,它就是中间件:

import { createStore, applyMiddleware } from "redux";
import { reducer } from "./reducer";
import ReduxLogger from "redux-logger";
//使用applyMiddleware加载中间件
let store = createStore(reducer, applyMiddleware(ReduxLogger));

这样在每一次更新state,会在控制台打印更新日志:

redux-logger

redux-thunk

redux-thunk中间件可以支持异步action。

加载中间件:

import reduxThunk from "redux-thunk";
let store = createStore(reducer, applyMiddleware(reduxThunk));

当加载了redux-thunk中间件,action函数可以支持返回一个函数,将异步操作封装在里面:

function add(payload) {
    return function(dispatch, getState) {
      setTimeout(() => {
        dispatch({ type: ADD, payload });
      }, 2000); //延时2秒执行
    };
}

可以看到action又返回一个函数,其中的参数dispatch和getState就是redux提供的方法,它将这两个函数的使用权交给了我们,让我们等待异步操作完成 时再调用,完成异步action编写。

redux-promise

有了redux-thunk中间件我们可以编写异步action,但我们想更进一步,让异步action支持Promise,那么redux-promise中间件可派上用场。

还是安装redux-promise然后加载它:

import reduxPromise from "redux-promise";
let store = createStore(
  reducer,
  applyMiddleware(reduxPromise)
);

redux-promise中间件可以支持action返回的对象payload为一个Promise:

let action = {
  add: function(payload) {
    return {
      type: ADD,
      //payload是一个Promise对象,异步操作封装到里面
      payload: new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(payload); //执行成功,将参数传到reducer
        }, 1000);
      })
    };
  },
  minus: function(payload) {
    return {
      type: MINUS,
      payload: new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(payload); //执行失败,将参数传到reducer
        }, 1000);
      })
    };
  }
};

可以看到,payload不再是直接返回参数,而是改为一个Promise对象,这样就可以把异步代码封装到里面。

注意!如果你使用redux-promise中间件,payload参数名是固定的,不可随意改名

比如:

{
  type: MINUS,
  num: new Promise((resolve, reject) => {
    //...
  })
};//此处参数名是num,redux-promise不能正确识别,若使用redux-promise必须叫payload

中间件的原理

通过以上三个中间件,可以清楚了它们的用法,都是在state更新的前后扩展一些功能,那么它们的原理是什么呢?

拿第一个中间件redux-logger来举例,日志是打印在state更新的前后,那么改写store.dispatch()方法是一个方案:

let temp = store.dispatch;//暂存原dispatch方法
store.dispatch = function(action) {
  console.log("旧state:", store.getState());
  temp(action);//执行原dispatch方法
  console.log("新state:", store.getState());
};

可以看到,首先将原来的dispatch方法临时保存到了变量中,并将现有的dispatch方法改写,增加了输出日志的功能,在state未更新之前先输出,再调用暂存的dispatch更新state即可,这样就相当于实现了redux-logger中间件。

虽然这种写法很恶心,但是这就是redux中间件的原理:暂存原dispatch方法,修改dispatch扩展功能并返回

中间件的通用写法

原理明白了,但是每次都手动去覆盖dispath显然太过麻烦,有没有通用的写法呢?显然是有的。

redux源码中是使用高阶函数去实现一个中间件,它的方法签明是这样的:


let middleware = store => next => action => {
    //具体中间件的逻辑...
};

可以看到,箭头函数的写法非常优雅,它是一个三层嵌套的函数,也就是高阶函数,它的最终返回值仍是一个方法,这个方法就是最终“扩展”了功能的“dispatch”方法。

不好理解?我们可以写成普通函数的形式,更容易看清逻辑:

function middleware(store) {
  //next为原dispatch方法
  return function(next) {
    //action为传入派发器的action对象
    return function(action) {
      //中间件的具体逻辑写在这儿...
    };
  };
}

即然中间件就是改写原dispath方法,那么我们可以想一想,要想扩展原来的dispath都需要哪些东西?应该是以下这些:

  • store仓库对象(有了store对象才能覆盖之前的dispath方法)
  • dispatch方法(之前的dispatch)
  • action对象 (派发动作需要action对象)

以上这三个对象必不可少,可以看到这三个对象正是三层函数的参数。第一层store参数实际是createStore()的返回值,就是仓库;第二层的next参数就是原dispatch方法;最内层的函数参数则是action对象。

搞清楚了方法签名的结构,我们就可以自己写出一个redux-logger中间件:

export function reduxLogger(store) {
  //next为原dispatch方法
  return function(next) {
    //action为传入派发器的action对象
    return function(action) {
      console.log("更新前:", store.getState());
      next(action);
      console.log("更新后:", store.getState());
    };
  };
}

很简单,在next()执行的前后打印state的状态即可。

applyMiddleware方法

我们手写了一个中间件,还要在需要时加载中间件,在redux中提供了一个applyMiddleware方法来加载中间件:

applyMiddleware(reduxLogger);

将所需的中间件依次传入即可加载中间件。那么它的原理呢?不防也来看一看。

applyMiddleware的方法签名仍是一个三层的高阶函数,

let applyMiddleware = middlewares => createStore => reducer => {
  //加载中间件的逻辑...  
};

还是一样,我们改写成普通函数来分析:

function applyMiddleware(middlewares) {
  //createStore即redux提供的方法
  return function(createStore) {
    //reducer就是传入更新state的函数
    return function(reducer) {
      //加载中间件的逻辑...  
    };
  };
}

想一下,在应用中间件的过程中,目的就是将外界传入的中间件的新dispath方法覆盖原有的store.dispatch,这样返回给用户的store对象的dispatch方法已经由中间件扩展了,比如这里就是打印日志。

那么applyMiddleware都需要哪些东西呢?

  • 需要应用的中间件
  • createStore方法(有了它就可以创建store对象)
  • reducer(创建store对象时需要reducer参数)

可以看到这些正是三层高阶函数的参数,这样我们就可以写出applyMiddleware的逻辑:

function applyMiddleware(middlewares) {
  return function(createStore) {
    return function(reducer) {
      let store = createStore(reducer); //取得store对象
      let dispatch = middlewares(store)(store.dispatch); //取得新的dispatch方法
      return { ...store, dispatch }; //将新dispatch覆盖旧的store.dispatch
    };
  };
}

其中重点是这一条语句:

let dispatch = middlewares(store)(store.dispatch);

这条语句就是取得中间件改写后的dispatch方法,还记得中间件的签名么?不防对照它来看一下就会明白:

let middleware = store => next => action => {};

中间件要求传入第一个参数store对象,通过createStore(reducer)创建; 中间件要求传入第二个参数next,就是原dispatch,那么store.dispatch就是原仓库的dispatch。此时返回的结果就是新dispath方法了,最后使用展开运算符将原store对象上的dispatch覆盖并返回即可。

到此,手写中间件和应用中间件的全部原理已经分析完毕。