本文主要是阅读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流程
- applyMiddleware封装之后
可以看出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"}
用图形象点就是
这样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处理...
-
用法
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下来操作一番。链接
先到这,下次衍生就是函数式编程了。