探究 redux 源码 - 衍生 - 中间件思想

998 阅读8分钟

本文主要是阅读redux实现方式的时候,思路的一些拓展。大概是在三四个月前就看过redux源码,一直想写一些东西。但是迫于项目的紧急性,以及个人能力精力有限,就搁浅了。现在又重新看,而且很多时候,看懂一些东西可能不难,但是真正深入进去研究,会发现很多东西并不是很清楚,这就需要多思考一些,再写下来能有清晰的思路就更难了。这次的文章需要你对redux,react-redux都有一定的了解,很多地方我没有做过多的解释,还有本文不完美的地方,还请指出。

redux基础

  • 我们先大概过一下redux暴露的几个方法。
// index.js
export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
}
  • createStore 一个工厂函数传入reducer,创建store,返回几个函数,主要是dispatch,getState,subscribe,replaceReducer,以及结合rx这种发布订阅库的symbol(?observable)

  • combineReducers 把单个的reducer组合成一个大的reducer

  • bindActionCreators 把我们写的一个js中的好多ActionCreator 通过遍历搞的一个对象里,并返回。

  • applyMiddleware 一个三阶函数,是用来改写store的dispatch方法,并把所有的中间件都compose串联起来,通过改写dispatch,来实现redux功能的拓展。

  • compose 一个组合多个middleware的方法,通过reduceRight方法(同理也可以是reduce),把传进来的middleware串成一条链,也可以看成回调接回调,一个预处理的方法。

redux-middleware

接触过后端的同学,对中间件这个概念一定不陌生。像node中的express,koa框架,middleware都起到了重要作用。redux中的实现方式不太一样,不过原理思想都是差不多的,都是链式组合,可以应用多个中间件。它提供了action发起之后,到达reducer之前的拓展功能。可以利用Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。

我们从redux中applyMiddleware使用入口开始研究。

中间件

  //日志中间件1
  const logger1 = store => next => action => {
    console.log('logger1 start', action);
    next(action);
    console.log('logger1 end', action);
  }

  //日志中间件2
  const logger2 = store => next => action => {
    console.log('logger2 start', action);
    next(action);
    console.log('logger2 end', action);
  }

为什么中间件要定义成这种三阶的样子呢,当然是中间件的消费者(applyMiddleware)规定的。

先通过一个小栗子看一下middleware的使用。

  //定义一个reducer
  const todoList = [];
  function addTodo(state = todoList, action) {
    switch (action.type) {
      case 'ADD_TODO':
        return [...state, action.text];
        break;
      default:
        return state;
    }
  }

 //创建store
 //为了先减轻其他方法带来的阅读困难,我选用直接使用applyMiddleware的方法创建store

  import { createStore, applyMiddleware } from 'redux';

  const store = applyMiddleware(logger1, logger2)(createStore)(reducer);


 // store注入Provider
  ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));

通过applyMiddleware执行可以得到一个store,store再通过react-redux中的provider注入。此时得到的store就是被改造了dispatch的。通过图来形象的解释一下:

  • 默认的redux流程

default

  • applyMiddleware封装之后

middleware1

可以看出redux在事件或者某个函数调用后,执行action(可能是bindActionCreators处理后的),由于bindActionCreator会去调用dispatch,

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

dispatch内部会把currenReducer执行,并把监听者执行。实现view更新。 但是经过applyMiddleware的包装,store里面的被封装,在调动action之后,执行封装后的dispatch就会经过一系列的中间件处理,再去触发reducer。

然后我们再通过研究源码,看他是怎么实现的封装dispatch。

思路可以从通过applyMiddleware创建store一点一点的看。

//applyMiddleware 源码

middlewares => createStore => (reducer, preloadedState) => {

 // 第一步先创建一个store
  var store = createStore(reducer, preloadedState, enhancer)

 // 缓存dispatch,原store的dispatch要改写。
  var dispatch = store.dispatch

 // 定义chain来存放 执行后的二阶中间件
  var chain = []

 // middleware 柯理化的第一个参数。参照logger1的store,这里只保留getState,和改造后的dispatch两个方法。
  var middlewareAPI = {
    getState: store.getState,
    dispatch: (action) => dispatch(action)
  }

  // 把中间件处理一层,把getState,dispatch方法传进去,也就是中间件柯理化第一次的store参数。
  // 这样能保证每个中间件的store都是同一个,柯理化的用途就是预置参数嘛。
  chain = middlewares.map(middleware => middleware(middlewareAPI))

  // 串联起所有的中间件,dispatch重新赋值,这样调用dispatch的时候,就会穿过所有的中间件。
  dispatch = compose(...chain)(store.dispatch)

  return {
    ...store,
    dispatch
  }
}

compose还是比较重要的

//compose
其实compose是函数式编程中比较重要的一个方法。上面调用compose的时候可见是一个二阶函数。

const compose = (...funcs) => {

  //没有参数,那就返回一个function
  if (!funcs.length) {
    return arg => arg
  }
  //一个中间件,返回它
  if (funcs.length === 1) {
    return funcs[0];
  }
  // 最后一个
  var last = funcs[funcs.length -1];

  // 复制一份,除去last
  var rest = funcs.slice(0, -1);

  // 返回函数,可以接收store.dispatch。
  // reduceRight 反向迭代。

  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}

compose执行

  • chain中都是已经预置middlewareAPI参数后的二阶函数。执行传入的参数都是 形参next。

  • 通过执行compose(...chain)(store.dispatch),last是最后一个中间件,执行并传入 store.dispatch, 返回一个只剩一阶的(action) => {}, 不过已经预置了next参数,也就是store.dispatch

  • 然后last(...args)返回的结果传入reduceRight的回调, 对应形参是composed。

  • f是rest的最后一项, 执行并把 composed 传入,等同于f形参中的next... 得到的结果也是一阶函数,预置的next是last(...args) ...

  • 以此类推。这样,就形成了一个嵌套多层的语句。
    类似于logger1(logger2(store.dispatch),当然这只是一个比喻。
    只不过到第一个middleware的时候,是二阶函数传入next执行,得到一阶函数返回赋值给dispatch,这时的一阶函数已经变成了形似这样:

    function (action) {
      console.log('logger1 start', action);
      next(action);
      console.log('logger1 end', action);
    }
    

经过compose之后的dispatch执行

  • 返回的store中dispatch被修改,执行store.dispatch的时候,也就是这个函数执行.

  • 当执行到next(action)的时候,会调用已经预置的next函数,也就是第二个中间件的(action) => {},依次类推。直到最后一个中间件,他的next函数是store.dispatch函数,执行并把action传入。

  • 执行完最后一个中间件的next(action),也就是初始的dispatch。next后面的代码再执行,再逆向把中间件走一遍,直到第一个中间件执行完毕。 就会出现这种效果

    start logger1 Object {type: "ADD_TODO", text: "defaultText"}
    start logger2 Object {type: "ADD_TODO", text: "defaultText"}
    dispatch()
    end logger2 Object {type: "ADD_TODO", text: "defaultText"}
    end logger1 Object {type: "ADD_TODO", text: "defaultText"}
    

用图形象点就是

middleware

这样redux middleware的执行流程就搞清楚了。

应用applyMiddleware的方式

import { createStore, applyMiddleware } from 'redux';

1. compose(applyMiddleware(logger1, logger2))(createStore)(reducer);

2. applyMiddleware(logger1, logger2)createStore)(reducer);

3. createStore(reducer, [], applyMiddleware(logger1, logger2));

createStore源码中有一个判断,

createStore(reducer, preloadedState, enhancer) => {
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // 所以第三种直接传入applyMiddleware(logger1, logger2),效果是一样的。
    return enhancer(createStore)(reducer, preloadedState)
  }
}

第一种先compose同理。一个参数的时候会返回applyMiddleware,变形之后也是一样的。

enhancer的用法很多种,不仅仅是applyMiddleware,比如Redux-Devtools, 都是利用了compose函数。自定义开发一些拓展功能还是很强大的... redux里的compose是处理三阶函数的,恰巧createStore, applyMiddleware都是三阶函数,都可以通过compose串联起来。不禁感叹函数式编程思维的强大啊。

应用异步action

  • redux-thunk

简单来说,就是dispatch(action), action 可以是function. 当然这种写法需要配合bindActionCreator处理。

actionCreator之前都是返回一个{type: 'UPDATE', text: 'aaa'}这样的简单对象。通过thunk中间件,可以处理返回function的情况。

const reduxThunk = store => next => action => {
  if (typeof action === 'function') {
    console.log('thunk');
    return action(store.dispatch);
  }
  return next(action);
}

//action 可能是这样。
const addAsync = function() {
  return (dispatch) => {
    setTimeout(() => {
      dispatch({ type: 'ADD_TODO', text: 'AsyncText' })
    }, 1000)
  }
}

  • redux-promise

用来处理actions返回的是promise对象的情况。其实道理很简单,thunk去判断传进中间件的action是不是function,这里就判断是不是promise就行了。

//判断promise
function isPromise(val) {
  return val && typeof val.then === 'function';
}

const reduxPromise = store => next => action => {
  return isPromise(action)
    ? action.then(store.dispatch)
    : next(action);
}

// 源码还多了一个判断,判断action是不是标准的flux action对象(简单对象,包含type属性...)

express中的middleware

当一个客户端的http请求过来的时候,匹配路由处理前后,会经过中间件的处理,比如一些CORS处理,session处理...

express-mid

  • 用法

    var app = express();
    
    app.use(function (req, res, next) {
      console.log('Time:', Date.now());
      next();
    });
    
    app.use(middleware1);
    app.use(middleware2);
    app.use(middleware3);
    
    app.listen(3000);

    每次访问这个app应用的时候,都会执行

  • 模拟

看了源码,自己模拟一下,当然是很简单的用法了。这是应用层的中间件,要实现路由器层的话,只需要根据路由 保存不同的数组就好了,然后匹配。

const http = require('http');
function express () {
  const app = function(req, res) {
    let index = 0;
    //重点在于next函数的实现,express是用一个数组维护的。
    function next() {
      const routes = app.route;
      routes[index++](req, res, next);
    }
    next();
  };

  app.route = [];

  // 很明显use 是往数组里push。
  app.use = function (callback) {
    this.route.push(callback);
  };

  // listen函数是一个语法糖,利用http模块
  app.listen = function(...args) {
    http.createServer(app).listen(...args);
  }

  return app;
}

const app = express();

app.use((req, res, next) => {
  setTimeout(() => {
    console.log('async');
    next();
  }, 1000);
});

app.use((req, res, next) => {
  console.log( 'logger request url:', req.url);
  next();
});


app.listen(3333);

假总结

现在web的中间件概念,都区别于最早严格意义上的中间件,其实我们现在的很多编程思想都是借鉴的先驱提出的一些东西。JAVA中类似的是AOP,即面向切面编程,以补充OOP(面向对象)多个对象公用某些方法时造成的耦合。

目前js中见到的中间件思想用法都是差不多的,只有调用next,程序才会继续往下执行,没有next,可以抛出异常等。只不过redux使用的函数式编程思想,用法偏函数式一些。

demo代码我会放到middleware-demo目录里,可以clone下来操作一番。链接

先到这,下次衍生就是函数式编程了。