中间件是个什么东西?
redux中管理数据的流程是单向的,就是说,从派发动作一直到发布订阅触发渲染是一条路走到头,那么如果想要在中间添加或是更改某个逻辑就需要找到action或是reducer来修改,有没有更方便的做法呢?
redux的流程:
button -触发事件-> dispath -派发动作-> reducer -发布订阅-> view
而中间件(middleware)就是一个可插拔的机制,如果想找扩展某个功能,比如添加日志,在更新前后打印出state状态,只需要将日志中间件装到redux上即可,于是便有了日志功能,当不想使用时可再拿掉,非常方便。
中间件的使用
先说说用法,只有会用了,再说原理。
redux-logger
redux提供了好多个现成的中间件,比如上面提到的日志中间件,安装它即可使用:
npm i --save redux-logger
复制代码
redux包提供了一个方法可以装载中间件:applyMiddleware
在创建store对象的时候,可以传入第二个参数,它就是中间件:
import { createStore, applyMiddleware } from "redux";
import { reducer } from "./reducer";
import ReduxLogger from "redux-logger";
//使用applyMiddleware加载中间件
let store = createStore(reducer, applyMiddleware(ReduxLogger));
复制代码
这样在每一次更新state,会在控制台打印更新日志:
redux-thunk
redux-thunk中间件可以支持异步action。
加载中间件:
import reduxThunk from "redux-thunk";
let store = createStore(reducer, applyMiddleware(reduxThunk));
复制代码
当加载了redux-thunk中间件,action函数可以支持返回一个函数,将异步操作封装在里面:
function add(payload) {
return function(dispatch, getState) {
setTimeout(() => {
dispatch({ type: ADD, payload });
}, 2000); //延时2秒执行
};
}
复制代码
可以看到action又返回一个函数,其中的参数dispatch和getState就是redux提供的方法,它将这两个函数的使用权交给了我们,让我们等待异步操作完成 时再调用,完成异步action编写。
redux-promise
有了redux-thunk中间件我们可以编写异步action,但我们想更进一步,让异步action支持Promise,那么redux-promise中间件可派上用场。
还是安装redux-promise然后加载它:
import reduxPromise from "redux-promise";
let store = createStore(
reducer,
applyMiddleware(reduxPromise)
);
复制代码
redux-promise中间件可以支持action返回的对象payload为一个Promise:
let action = {
add: function(payload) {
return {
type: ADD,
//payload是一个Promise对象,异步操作封装到里面
payload: new Promise((resolve, reject) => {
setTimeout(() => {
resolve(payload); //执行成功,将参数传到reducer
}, 1000);
})
};
},
minus: function(payload) {
return {
type: MINUS,
payload: new Promise((resolve, reject) => {
setTimeout(() => {
reject(payload); //执行失败,将参数传到reducer
}, 1000);
})
};
}
};
复制代码
可以看到,payload不再是直接返回参数,而是改为一个Promise对象,这样就可以把异步代码封装到里面。
注意!如果你使用redux-promise中间件,payload参数名是固定的,不可随意改名
比如:
{
type: MINUS,
num: new Promise((resolve, reject) => {
//...
})
};//此处参数名是num,redux-promise不能正确识别,若使用redux-promise必须叫payload
复制代码
中间件的原理
通过以上三个中间件,可以清楚了它们的用法,都是在state更新的前后扩展一些功能,那么它们的原理是什么呢?
拿第一个中间件redux-logger来举例,日志是打印在state更新的前后,那么改写store.dispatch()
方法是一个方案:
let temp = store.dispatch;//暂存原dispatch方法
store.dispatch = function(action) {
console.log("旧state:", store.getState());
temp(action);//执行原dispatch方法
console.log("新state:", store.getState());
};
复制代码
可以看到,首先将原来的dispatch方法临时保存到了变量中,并将现有的dispatch方法改写,增加了输出日志的功能,在state未更新之前先输出,再调用暂存的dispatch更新state即可,这样就相当于实现了redux-logger中间件。
虽然这种写法很恶心,但是这就是redux中间件的原理:暂存原dispatch方法,修改dispatch扩展功能并返回。
中间件的通用写法
原理明白了,但是每次都手动去覆盖dispath显然太过麻烦,有没有通用的写法呢?显然是有的。
redux源码中是使用高阶函数去实现一个中间件,它的方法签明是这样的:
let middleware = store => next => action => {
//具体中间件的逻辑...
};
复制代码
可以看到,箭头函数的写法非常优雅,它是一个三层嵌套的函数,也就是高阶函数,它的最终返回值仍是一个方法,这个方法就是最终“扩展”了功能的“dispatch”方法。
不好理解?我们可以写成普通函数的形式,更容易看清逻辑:
function middleware(store) {
//next为原dispatch方法
return function(next) {
//action为传入派发器的action对象
return function(action) {
//中间件的具体逻辑写在这儿...
};
};
}
复制代码
即然中间件就是改写原dispath方法,那么我们可以想一想,要想扩展原来的dispath都需要哪些东西?应该是以下这些:
- store仓库对象(有了store对象才能覆盖之前的dispath方法)
- dispatch方法(之前的dispatch)
- action对象 (派发动作需要action对象)
以上这三个对象必不可少,可以看到这三个对象正是三层函数的参数。第一层store参数实际是createStore()
的返回值,就是仓库;第二层的next参数就是原dispatch方法;最内层的函数参数则是action对象。
搞清楚了方法签名的结构,我们就可以自己写出一个redux-logger中间件:
export function reduxLogger(store) {
//next为原dispatch方法
return function(next) {
//action为传入派发器的action对象
return function(action) {
console.log("更新前:", store.getState());
next(action);
console.log("更新后:", store.getState());
};
};
}
复制代码
很简单,在next()
执行的前后打印state的状态即可。
applyMiddleware方法
我们手写了一个中间件,还要在需要时加载中间件,在redux中提供了一个applyMiddleware方法来加载中间件:
applyMiddleware(reduxLogger);
复制代码
将所需的中间件依次传入即可加载中间件。那么它的原理呢?不防也来看一看。
applyMiddleware的方法签名仍是一个三层的高阶函数,
let applyMiddleware = middlewares => createStore => reducer => {
//加载中间件的逻辑...
};
复制代码
还是一样,我们改写成普通函数来分析:
function applyMiddleware(middlewares) {
//createStore即redux提供的方法
return function(createStore) {
//reducer就是传入更新state的函数
return function(reducer) {
//加载中间件的逻辑...
};
};
}
复制代码
想一下,在应用中间件的过程中,目的就是将外界传入的中间件的新dispath方法覆盖原有的store.dispatch
,这样返回给用户的store对象的dispatch方法已经由中间件扩展了,比如这里就是打印日志。
那么applyMiddleware都需要哪些东西呢?
- 需要应用的中间件
- createStore方法(有了它就可以创建store对象)
- reducer(创建store对象时需要reducer参数)
可以看到这些正是三层高阶函数的参数,这样我们就可以写出applyMiddleware的逻辑:
function applyMiddleware(middlewares) {
return function(createStore) {
return function(reducer) {
let store = createStore(reducer); //取得store对象
let dispatch = middlewares(store)(store.dispatch); //取得新的dispatch方法
return { ...store, dispatch }; //将新dispatch覆盖旧的store.dispatch
};
};
}
复制代码
其中重点是这一条语句:
let dispatch = middlewares(store)(store.dispatch);
复制代码
这条语句就是取得中间件改写后的dispatch方法,还记得中间件的签名么?不防对照它来看一下就会明白:
let middleware = store => next => action => {};
复制代码
中间件要求传入第一个参数store对象,通过createStore(reducer)
创建; 中间件要求传入第二个参数next,就是原dispatch,那么store.dispatch就是原仓库的dispatch。此时返回的结果就是新dispath方法了,最后使用展开运算符将原store对象上的dispatch覆盖并返回即可。
到此,手写中间件和应用中间件的全部原理已经分析完毕。