Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
sunyongjian opened this issue Jun 8, 2017 · 2 comments
Open

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

sunyongjian opened this issue Jun 8, 2017 · 2 comments
Labels

Comments

@sunyongjian
Copy link
Owner

sunyongjian commented Jun 8, 2017

本文主要是阅读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。其实就是返回一个新的reducer函数,去遍历执行所有的子reducer,并返回state。

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

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

@sunyongjian sunyongjian changed the title 探究redux源码衍生-中间件思想 探究redux源码-衍生-中间件思想 Jun 10, 2017
@dioxide
Copy link

dioxide commented Jun 12, 2017

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

@sunyongjian
Copy link
Owner Author

@dioxide

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

这就是三阶函数。

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants