昨天的文章手写了一版redux的核心源码,redux库除了数据的状态管理还有一块重要的内容那就是中间件,今天我仍是尝试将此部分源码完成。javascript
react中管理数据的流程是单向的,就是说,从派发动做一直到发布订阅触发渲染是一条路走到头,那么若是想要在中间添加或是更改某个逻辑就须要找到action或是reducer来修改,有没有更方便的作法呢?java
而中间件(middleware)就是一个可插拔的机制,若是想要扩展某个功能,好比添加日志,在更新先后打印出state状态,只须要将日志中间件装到redux上便可,因而便有了日志功能,当不想使用时可再拿掉,很是方便。react
目前有不少第三方的中间件安装便可使用,好比刚刚说起的日志中间件:redux-logger,使用npm安装它:面试
npm install redux-logger
npm
redux包提供了一个方法能够装载中间件:applyMiddleware。在建立store对象的时候,能够传入第二个参数,它就是中间件:编程
import { createStore, applyMiddleware } from "redux"; import { reducer } from "./reducer"; import ReduxLogger from "redux-logger"; //使用applyMiddleware加载中间件 let store = createStore(reducer, applyMiddleware(ReduxLogger));
装载好中间件就在派发动做上扩展了相应的功能,这时咱们正常编写redux程序,当执行dispatch方法时会在控制台打印出state更新日志:redux
以上就是一个使用中间件的例子。segmentfault
那么中间件的执行原理是什么呢?就用刚刚的日志中间件举例,它的功能是在state对象的更新先后分别输出状态,那么确定是在派发(dispatch)动做的那一刻去实现的,那咱们改写一下redux库,将“打印日志”功能添加到dispatch方法里:app
let temp = store.dispatch;//暂存原dispatch方法 store.dispatch = function(action) { console.log("旧state:", store.getState()); temp(action);//执行原dispatch方法 console.log("新state:", store.getState()); };
这样就实现了“日志中间件”,可是直接改写redux库是不可能的,咱们须要一个通用的办法去定义中间件,redux提供了这样一个方法:applyMiddleware。框架
它的使用方法很简单,将须要加载的中间件依次传入applyMiddleware方法中便可:
applyMiddleware(ReduxLogger, ReduxThunk);
中间件原理咱们分析完了,即然中间件就是扩展dispatch方法,那么applyMiddlware必然会将中间件的dispatch方法和原始dispatch传入才可行,没错,咱们就看看它的方法签名:
var applyMiddleware = (middlewares) => (createStore) => (reducer) => {};
以上就是applyMiddleware方法,它又是一个三层的高阶函数,这里用到了函数柯里化的思想,将多个参数拆分为单一参数的高阶函数,以保证每一层只有一个参数,这样更加灵活可分块调用。写成箭头函数很差理解,咱们改写为普通函数形式:
var applyMiddleware = function (middlewares){ return function (createStore){ return function (reducer){ //在这里装载中间件 } } };
经过函数参数就能够看到,三层函数分别传入了中间件(middleware)、建立仓库方法(createStore)和reducer函数,这正是咱们装载一个中间件所须要的。
接下来咱们的目标就是将中间件提供的dispatch覆盖redux原有的dispatch方法,这样就“装载”好了中间件。
var applyMiddleware = function (middlewares) { return function (createStore) { return function (reducer) { let store = createStore(reducer); //调用中间件,返回新dispatch方法 let newDispatch = middlewares(store)(store.dispatch); //覆盖原有的dispatch方法并返回仓库对象 return { ...store, dispatch: newDispatch } } } }
有了通用写法,咱们本身模拟实现一个日志中间件:
function reduxLogger(store) { return function (dispatch) { //dispatch参数即原redux派发方法 return function (action) { //返回的这个函数即新方法 //最终会传入applyMiddleware覆盖掉dispatch console.log(`更新前:${JSON.stringify(store.getState())}`); dispatch(action); console.log(`更新后:${JSON.stringify(store.getState())}`); } } }
调用咱们本身的方法装载中间件:applyMiddleware(reduxLogger);
,运行效果以下:
可是到如今还没完,还记得官方redux库吗?人家的applyMiddlewares方法是支持传入多个中间件的,如:applyMiddlewares(middleware1,middleware2);
咱们目前的方法还不支持这种写法,最终的目的是想把若干个中间件一次组合为一个总体,一块儿加载。
洋葱模型的概念彷佛是在Koa2框架中提出的,它是指中间件的执行机制,当多个中间件执行时,后一个中间件会套在前一个中间件的里面:
执行完一个中间件会一直向里走,直到最后一个执行结束,再从内而外走出,就像是在剥洋葱同样。
咱们一样使用洋葱模型来写一个组合方法,以达到目的。
新建一个compose.js,建立一个组合函数:
/** * 组合全部中间件 * @param {...any} middlewares */ function compose(...middlewares) { return function (...args) { } }
个人目标是当调用组合函数,传入多个中间件,将全部的中间件组合成一个函数:
var all = compose(middleware3, middleware2, middleware1); all();//调用时,依次执行全部中间件
咱们动手实现它。
写以前咱们先想一下,组合功能便是将“若干”个功能封装为“一个”功能,这正是函数式编程的收敛思想,ES6中已经为咱们提供了reduce函数,在这里最合适不过了:
/** * 组合全部中间件 * @param {...any} middlewares */ function compose(...middlewares) { //args即第一个中间件所需参数 return function (...args) { return middlewares.reduce((composed, current) => { return function (...args) { //当前中间件的执行结果即上一个中间件的参数 return composed(current(...args)); } })(...args); } }
经过reduce函数,一步一步将后一个中间件套到前一个中间件之中,后一个中间件的结果即前一个的参数,这样层层递近,最终返回一个大函数,即完成组合。
最后能够优化为箭头函数的形式,显得逼格更高一点:
function compose(...middlewares) { return (...args) => middlewares.reduce((composed, current) => (...args) => composed(current(...args)))(...args) }
在compose完成以后,最后一步的工做就是改写applyMiddlewares将全部传入的中间件组合好:
function applyMiddleware(...middlewares) { return function (createStore) { return function (reducer) { let store = createStore(reducer); //一次传入多个中间件,循环包开一层函数 let chain = middlewares.map(middleware => { return middleware(store); }); //组合全部的中间件 let newDispatch = compose(...chain)(store.dispatch); //覆盖原有的dispatch方法 return { ...store, dispatch: newDispatch } } } }
至此,redux库的源码已经基本实现完毕。
多个中间件运行以下:
这两天从头手写了一遍redux库发现redux的源码量并不大可是逻辑仍是很复杂的,理清redux的流程是读写源码的前提。而中间件则是redux库的一个难点,主要是层层调用关系很是恼人,一个好办法是经过库源码与中间件源码对比来分析,理清思路便可,若是还有时间我会尝试再手写一版react-redux库,一个是学习提升,二是应付面试。