中间件与异步操作
由于国庆前项目要上线,加上随之而来的国庆假期,redux系列的学习总结就被耽误了 ~~~今天终于可以抽出时间来整理总结了。本篇文章先从中间件说起,什么是中间件,为什么需要中间件、中间件解决了哪些问题,进而进入redux中的异步操作。
Redux中的中间件
在搞明白什么是中间件之前先看一个小需求: 想做一个类似日志功能,记录每次用户操作的action
和dispatch
之后的state
值。在上一篇文章中我们知道用户dispatch
一个action
后,就会立即被reducer
函数处理并返回一个新的state
,接着调用subscribe
中的监听函数执行页面的更新操作。
源码中的dispatch
方法内部并没有给我们提供类似可扩展的功能,所以我们只能在用户调用dispatch
前后,手动添加日志
console.log("prev state", store.getState())
const action = store.dispatch(action)
console.log("action", action)
console.log("next state", store.getState())
然后对功能进行封装
function enhancerDispatch(store, action) {
console.log("prev state", store.getState())
console.log("action", action)
store.dispatch(action)
console.log("next state", store.getState())
}
虽然功能我们已经封装好了,但这样的做法还是需要在每次使用时导入enhancerDispatch
函数才行,有点麻烦呀,如果我们直接修改原store.dispatch
的方法呢?是不是就只用在引入store
时修改一次就可以了呢
const next = store.dispatch; //将store.dispatch原先的引用赋值给一个新的变量
store.dispatch = function (store, action) { //将一个我们自己定义的函数赋值给store.dispatch
console.log("prev state", store.getState())
console.log("action", action)
next(action) //执行原dispatch方法
console.log("next state", store.getState())
}
我们用自己的函数替换了store.dispatch
,确实以后我们不管在任何地方使用store.dispatch
都会带有日志的功能了;继续 我们再实现一个异常捕获的功能,显然这些功能最好都是彼此独立的,既可以同时使用,又能单独调用。这样的话那就不能将异常捕获的功能也参杂到日志记录的功能中了,最好以模块的形式将彼此分开单独写。
//日志
function loggerDispatch(store) {
const next = store.dispatch; //将store.dispatch原先的引用赋值给一个新的变量
store.dispatch = function (action) { //将一个函数赋值给store.dispatch
console.log("prev state", store.getState())
console.log("action", action)
const result = next(action) //执行原dispatch方法, 默认返回action
console.log("next state", store.getState())
return result
}
}
//异常
function errorDispatch(store) {
const next = store.dispatch; //将store.dispatch原先的引用赋值给一个新的变量
store.dispatch = function (action) { //将一个新函数赋值给store.dispatch
console.log("error", action)
try {
return next(action) //执行原dispatch方法
} catch (err) {
console.log("error", err)
}
}
}
//让日志功能和异常报告功能同时生效
loggerDispatch(store)
errorDispatch(store)
开始的时候在这里我有一个困惑的地方,store.dispatch
的值是一个引用数据类型,那么在执行第一个函数loggerDispatch(store)
之后,我们就用自己的函数替换了原store.dispatch
所指向的函数, 在执行完第二个函数errorDispatch(store)
之后,最终store.dispatch
就指向了上面我们写的异常捕获函数了,那为啥当我在组件中调用store.dispatch
时,上面两个功能会同时生效呢?按理说store.dispatch
指向了异常捕获函数,那不是应该只有异常捕获功能有效吗???
后来发现是有一行关键的代码自己没有理解清楚忽略了,关键之处就在于next(action)
,下面就来看看他的关键之处:
//函数一
function loggerDispatch(store) {
const next = store.dispatch; //将原store.dispatch函数用一个变量保存
store.dispatch = function (action) { //将一个新函数赋值给store.dispatch
// ...
const result = next(action) //这里执行的就是上面保存的那个函数
}
}
//函数二
function errorDispatch(store) {
const next = store.dispatch; //注意这里next变量保存的就是函数一第4行赋值的新函数
store.dispatch = function (action) { //再一次将一个新函数赋值给store.dispatch
// ...
return next(action) //注意这里执行的就是函数一第4行赋值的新函数
}
}
//按顺序调用函数一、函数二
loggerDispatch(store)
errorDispatch(store)
所以当在组件中执行store.dispatch
时,此时的store.dispatch
函数已经被替换成函数二第13行赋值的新函数了,当函数执行到next(action)
时,这里实际上调用的是函数一中给store.dispatch
新赋值的函数,当在这个新函数中执行到next(action)
时,最后这里实际调用的才是原Redux
库为我们提供的dispatch
函数。我们也可以发现在这些功能函数被调用的时候,是由外往内在next(action)
前后添加功能, 但是最终在组件中调用dispatch
时,这些功能是由内往外体现,所以在console
控制台中应该是先打印error
,后打印logger
。
如果后面再需要添加别的功能,依然是像上面这样,用新函数替换掉Redux
库为我们提供的store.dispatch
。但是总感觉这种直接替换别人源码的方式怪怪的。我们上面之所以要用新函数替换掉原stroe.dispatch
,就是为了在后面可以操作或调用上一层的dispatch
方法 ,使所有扩展功能都生效。如果我们不去替换原来的dispatch
,而是在函数中返回一个新的dispatch
,然后类似像链式调用一样,在新函数中接受上一层返回的dispatch
作为参数,这样新函数内部不也可以获取上一层的dispatch
方法了吗。这里有没有一点 函数按照一定顺序并层层嵌套执行的感觉??想到了啥? 对!复合函数compose(将一个函数的返回值作为下一个函数的参数,并按照一定顺序执行)。
function logger(store) {
//之前的做法是用一变量保存
//const next = store.dispatch
return function(next) { //直接返回一个函数,函数接收上一层的dispatch作为参数
//之前的做法是直接用新函数替换
//store.dispatch = function (action) { ... }
return function (action) {
// ...logger功能
return next(action)
}
}
}
//改写成ES6的箭头函数
const logger = store => next => action => {
// ...logger功能
return next(action)
}
//error
const error = store => next => action => {
//error功能
return next(action)
}
上面说的logger
、error
就是我们所谓Redux
中的中间件,中间件就是一个函数, 该函数接受一个dispatch
函数作为参数,并返回一个新的dispatch
函数, 返回的函数会被作为一下个中间件的next
传入。我们也可以发现中间件实际上就是对原dispatch
的功能进行增强,在action
发出之后,reducer
函数执行之前搞点事情,最终我们用于派发action
的函数还是原Redux
库提供的store.dispatch
;
我之前这里一直有一个疑惑,为什么中间件函数需要有这么多层?第一层接收store
,第二层接收next
,第三层接收action
,但中间件本质上处理的就是action
对象,那为啥不直接把store
、next
放在同一层,然后返回一个函数,用于处理action
呢?像下面这样
const logger = (store, next) => action => {
consoe.log(store.getState(), action)
return next(action)
}
这样不是更容易理解么?其实Redux
是根据函数式编程的思想来写的,所以我在整理Redux
知识的时候,先复习了一下函数式编程 ,Redux
中用了好多函数式编程的思想来解决问题,而函数式编程一个重要的思想就是让函数的功能尽可能单一,然后再通过函数的嵌套组合来实现复杂的功能。
我们可以总结一下中间件的特点:中间件是一个独立的函数, 对store.dispatch
方法进行增强或改造;中间件可以组合使用,且结果跟中间件的执行顺序有一定关系;中间件具有统一的接口,都是接收一个dispatch
函数作为参数并返回一个新dispatch
函数
applyMiddleWare
上面在介绍中间件时我们就说了,需要将中间件返回的dispatch
函数,按照传入的顺序依次传入到下一个中间件,Redux
库为我们提供了一个applyMiddleware
方法;
function applyMiddleware(...middlewares) { //接收一些中间件作为参数
return function (createStore) {
return function () {
var store = createStore.apply(void 0, arguments);
//所有中间件功能都加载完,才能执行dispatch操作,否者就不完整了
var _dispatch = function dispatch() {
throw new Error('Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.');
};
var middlewareAPI = {
getState: store.getState,
dispatch: function dispatch() {
return _dispatch.apply(void 0, arguments);
}
};
//遍历执行中间件,为中间件传入其内部所需要的一些变量,像logger内部用到的getState
var chain = middlewares.map(function (middleware) {
return middleware(middlewareAPI);
});
//可以这样认为,上一步的遍历执行是为了生成中间件,现在chain中的元素才是中间件本身,由于中间件本身是一个接收dispatch并返回一个新dispatch的函数;这里经compose复合函数 使中间件按照参数接收位置,依次从右往左执行,例如:compose(logger, error) --> logger(error()),本身compose运行的结果就需要返回一个函数,而且最内层的error中间件,也需要传入一个dispatch函数,所以最后就变成了 dispatch => logger(error(dispatch))
_dispatch = compose(...chain)(store.dispatch);
//最终依然返回store, 只不过是对原有dispatch方法进行了增强
return _objectSpread2({}, store, {
dispatch: _dispatch
});
};
};
}
//放上compose函数的源码,之前文章里有解释,这里不说啦
//注意一点: redux中 中间件不是按照传入的顺序执行, 而是从右往左执行
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, c) => (args) => a(c(args)))
/**
return funcs.reduce(function (a, c) {
return function (args) {
return a(c(args));
};
});
*/
}
异步操作
理解了上面说的中间件后,异步操作就有了解决办法,我们知道原本dispatch
方法只能接收一个具有type
属性的对象作为参数,但是对于我们自己扩展的增强型dispatch
可以处理处理其他类型的参数:如果接收的action
是对象类型直接放行,可以由Redux
库默认的dispatch
方法执行,对于像函数类型的异步操作,直接执行该函数,待异步操作执行完毕之后,将异步操作返回的结果包装成action
对象,其巧妙之处就在于这个action
函数, 我们可以事先将dispatch
方法以参数的形式传入,再拿到异步操作结果后直接用传入的dispatch
函数调用;
const reduxThunk = store => next => action => {
if(typeof action === 'function') {
return action(store.dispatch) //注意当action是函数类型时, 需要传入一个dispatch函数作为参数, 以便在异步操作结束时好发送dispatch请求
}else {
return next(action)
}
}
createStore
虽然我们在上一篇文章中分析过createStore
的源码,但那时候省略了第三个参数enhancer
, 现在我们理解了中间件之后再回过头来看看添加enhancer
之后的处理方式:
function createStore(reducer, preloadedState, enhancer) {
//省略preloadedState时的逻辑判断,enhancer可以作为第二个参数传入
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState;
preloadedState = undefined;
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.');
}
//我们的enhancer就是applyMiddleware(logger, error),这是一个函数调用表达式,它返回的结果是一个函数,而且需要接收createStore,用于内部生成store。
// enhancer(createStore); 回到上面applyMiddleware,我们知道这里返回的也还是一个函数,需要接收reducer等参数,作为createStore函数的参数,生成store
return enhancer(createStore)(reducer, preloadedState);
}
/**
...
*/
}