sunyongjian / blog

个人博客😝😋😄
664 stars 54 forks source link

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

Open sunyongjian opened 7 years ago

sunyongjian commented 7 years ago

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

redux基础

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的。通过图来形象的解释一下:

default 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执行

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

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)
  }
}
//判断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

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下来操作一番。链接

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

dioxide commented 7 years ago

三阶函数,二阶函数是怎样的概念?

sunyongjian commented 7 years ago

@dioxide

const func = a => b => c => {
   //code
}

这就是三阶函数。

const func = a => b => {
   //code
}

这就是二阶。 看他要执行几次才能到最内部的函数代码,就是几阶。统称就是高阶函数...