目录
前言
一、redux 与 中间件 图解
二、中间件的概念
三、Redux 异步操作的基本思路
四、Redux 为什么使用中间件来处理异步数据流?
五、redux-thunk 中间件
六、redux-promise 中间件
七、redux-saga 中间件(推荐)
八、Redux 异步的其他学习资料
redux 只处理同步数据流,异步数据流交给其 “中间件” 处理。
redux 中间件经历了三次发展:
redux 图解 中间件(middleware)图解
关于“中间件的概念”源自阮一峰老师的文章《Redux 入门教程(二):中间件与异步操作》
为了理解中间件,让我们站在框架作者的角度思考问题:如果要添加功能,你会在哪个环节添加?
想来想去,只有发送 Action 的这个步骤,即store.dispatch()方法,可以添加功能。举例来说,要添加日志功能,把 Action 和 State 打印出来,可以对store.dispatch进行如下改造。
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
next(action);
console.log('next state', store.getState());
}
上面代码中,对store.dispatch进行了重定义,在发送 Action 前后添加了打印功能。这就是中间件的雏形。
中间件就是一个函数,对store.dispatch方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。
dispatch 一个 action 之后,到达 reducer 之前,进行一些额外的操作,就需要用到 middleware。你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。
redux的中间件是一个函数,该函数接收dispatch和getState作为参数,返回一个以dispatch为参数的函数,这个函数的返回值是接收action为参数的函数(可以看做另一个dispatch函数)。在中间件链中,以dispatch为参数的函数的返回值将作为下一个中间件(准确的说应该是返回值)的参数,下一个中间件将它的返回值接着往下一个中间件传递,最终实现了store.dispatch在中间件间的传递。
中间件只关注 dispatch 函数的传递,至于在传递的过程中干了什么中间件并不关心。
阮一峰老师在《Redux 入门教程(二):中间件与异步操作》写到:
【拓展】
JS 运行机制——同步与异步
明明可以直接调用 dispatch,为什么要使用中间件来处理异步数据流呢?
下面举一个“直接调用 dispatch”的例子(案例原文):
import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';
const ActionTypes = {
STARTED_UPDATING: 'STARTED_UPDATING',
UPDATED: 'UPDATED'
};
class AsyncApi {
static getFieldValue() {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(Math.floor(Math.random() * 100));
}, 1000);
});
return promise;
}
}
class App extends React.Component {
render() {
return (
{this.props.isWaiting && Waiting...}
);
}
}
App.propTypes = {
dispatch: React.PropTypes.func,
field: React.PropTypes.any,
isWaiting: React.PropTypes.bool
};
const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
switch (action.type) {
case ActionTypes.STARTED_UPDATING:
return { ...state, isWaiting: true };
case ActionTypes.UPDATED:
return { ...state, isWaiting: false, field: action.payload };
default:
return state;
}
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
(state) => {
return { ...state };
},
(dispatch) => {
return {
update: () => {
dispatch({
type: ActionTypes.STARTED_UPDATING
});
AsyncApi.getFieldValue()
.then(result => dispatch({
type: ActionTypes.UPDATED,
payload: result
}));
}
};
})(App);
export default class extends React.Component {
render() {
return ;
}
}
这种方法没有错,但是不够优雅。下面拿两个案例对比着进一步说明:
案例一:不使用中间件,直接调用 dispatch
// action creator
function loadData(dispatch, userId) { // 需要调度,所以这是第一个参数
return fetch(`http://data.com/${userId}`)
.then(res => res.json())f
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
loadData(this.props.dispatch, this.props.userId); // 别忘了通过调度
}
案例二:使用 redux-thunk 中间件
// action creator
function loadData(userId) {
return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk处理这些
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
this.props.dispatch(loadData(this.props.userId));
}
对比上述两个案例可知:中间件只关心 dispatch 的传递,并不限制你做其他的事情。thunk 函数只是更改了你的写法——不使用 thunk 你需要每次传递给异步函数 dispatch,使用 thunk 函数后在组件中就可以调用 dispatch,组件不再需要关注 dispatch 派发的函数是不是异步,组件只是发出一个请求而已。
redux-thunk 的基本思想就是通过函数来封装异步请求,也就是说在actionCreater中返回一个函数,在这个函数中进行异步调用。
redux 中间件只关注 dispatch 函数的传递,而且redux 也不关心 dispatch 函数的返回值。
redux-thunk 的源码(node_modules/redux-thunk/src/index.js):
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
解析成 ES5 的代码如下:
function createThunkMiddleware(extraArgument) {
return function({ dispatch, getState }) {
return function(next){
return function(action){
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
}
}
由上述代码可知:redux-thunk 在 actionCreater 中返回一个函数,在这个函数中进行异步调用。如果这个 actionCreater 传过来的action是一个函数的话,就执行它,并以这个函数的返回值作为返回值;如果不是,就按照原来的 next(action) 执行。
redux-thunk 的缺点:
主要因素
)。主要因素
)。
阮一峰老师在《Redux 入门教程(二):中间件与异步操作》写到:
既然 Action Creator 可以返回函数,当然也可以返回一个 Promise 对象,这就是 redux-promise 中间件。redux-promise 中间件就是 redux-thunk 与 promise 的结合。
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';
const store = createStore(
reducer,
applyMiddleware(promiseMiddleware)
);
这个中间件使得 store.dispatch 方法可以接受 Promise 对象作为参数。这时,Action Creator 有两种写法:
写法一:返回值是一个 Promise 对象。
const fetchPosts =
(dispatch, postTitle) => new Promise(function (resolve, reject) {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => {
type: 'FETCH_POSTS',
payload: response.json()
});
});
写法二:Action 对象的 payload 属性是一个 Promise 对象。这需要从 redux-actions 模块引入 createAction 方法,并且写法也要变成下面这样。
import { createAction } from 'redux-actions';
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
// 发出同步 Action
dispatch(requestPosts(selectedPost));
// 发出异步 Action
dispatch(createAction(
'FETCH_POSTS',
fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
));
}
上面代码中,第二个 dispatch 方法发出的是异步 Action,只有等到操作结束,这个 Action 才会实际发出。注意,createAction 的第二个参数必须是一个 Promise 对象。
看一下redux-promise 的源码,就会明白它内部是怎么操作的。
export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action)
? action.then(dispatch)
: next(action);
}
return isPromise(action.payload)
? action.payload.then(
result => dispatch({ ...action, payload: result }),
error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
}
)
: next(action);
};
}
从上面代码可以看出,如果 Action 本身是一个 Promise,它 resolve 以后的值应该是一个 Action 对象,会被dispatch方法送出(action.then(dispatch)),但 reject 以后不会有任何动作;如果 Action 对象的payload属性是一个 Promise 对象,那么无论 resolve 和 reject,dispatch方法都会发出 Action。
redux-saga 官网:https://redux-saga-in-chinese.js.org/
Redux-saga 的 API:https://redux-saga-in-chinese.js.org/docs/api/
redux-saga 用 generator 代替了 promise(babel的基础版本不包含generator语法,因此需要在使用saga的地方import ‘babel-polyfill’)。
redux-saga 将异步任务进行了 集中处理。
sagas 包含3个部分,用于联合执行任务:
redux-saga 的使用方法:
首先需要启动saga,启动saga一般都写在入口文件中,下面是个栗子:
import { createStore, applyMiddleware} from 'redux';
import appReducer from './reducers';
import createSagaMiddleware from 'redux-saga';
import rootSaga from "./sagas/rootSaga";
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
const store = createStore(appReducer,applyMiddleware(...middlewares));
sagaMiddleware.run(rootSaga);//saga一旦执行就会永远执行下去
render(
,
document.getElementById('app')
);
然后,就可以在 sagas 文件夹中集中写 saga 文件了:
import { take, fork, call, put } from 'redux-saga/effects';
//执行函数即work saga
function* fetchUrl(url) {
try{//利用try-catch来捕获异常
const data = yield call(fetch, url); // 指示中间件调用 fetch 异步任务
yield put({ type: 'FETCH_POSTS_SUCCESS', payload:data }); // 指示中间件发起一个 action 到 Store
}catch(e){
yield put({ type: 'FETCH_POSTS_FAILURE', payload:error })
}
}
// 监听函数即watch saga
function* watchFetchRequests() {
while(true) {
const action = yield take('FETCH_POSTS_REQUEST'); // 指示中间件等待 Store 上指定的 action,即监听 action
yield fork(fetchUrl, action.url); // 指示中间件以无阻塞调用方式执行 fetchUrl
}
}
在 redux-saga 中的基本概念就是:sagas 自身不真正执行副作用(如函数 call),但是会构造一个需要执行副作用的描述。中间件会执行该副作用并把结果返回给 generator 函数。
对于sages ,采用 Generator 函数来 yield Effects(包含指令的文本对象)。Generator 函数的作用是可以暂停执行,再次执行的时候从上次暂停的地方继续执行。Effect 是一个简单的对象,该对象包含了一些给 middleware 解释执行的信息。你可以通过使用 effects API 如 fork,call,take,put,cancel 等来创建 Effect。
关于Effect官方是这样解释的:
在 redux-saga 的世界里,Sagas 都用 Generator 函数实现。我们从 Generator 里 yield 纯 JavaScript 对象以表达 Saga 逻辑。 我们称呼那些对象为 Effect。Effect 是一个简单的对象,这个对象包含了一些给 middleware 解释执行的信息。 你可以把 Effect 看作是发送给middleware 的指令以执行某些操作(调用某些异步函数,发起一个 action 到 store)。
对上述例子的说明:
这里我们可以来看一波源码,搞清楚这个Effect到底是什么。
对于Effect对象的定义,写在了 redux-saga/src/internal/io.js 文件中,下面是Effect的定义。
const effect = (type, payload) => ({ [IO]: true, [type]: payload });
很简单就一句话,表明这个effect其实就是返回了一个对象。
接下来看看所谓的put和call到底是个什么东西:
put
export function put(channel, action) {
if (process.env.NODE_ENV === 'development') {
if (arguments.length > 1) {
check(channel, is.notUndef, 'put(channel, action): argument channel is undefined')
check(channel, is.channel, `put(channel, action): argument ${channel} is not a valid channel`)
check(action, is.notUndef, 'put(channel, action): argument action is undefined')
} else {
check(channel, is.notUndef, 'put(action): argument action is undefined')
}
}
if (is.undef(action)) {
action = channel
channel = null
}
return effect(PUT, { channel, action })
}
call
function getFnCallDesc(meth, fn, args) {
if (process.env.NODE_ENV === 'development') {
check(fn, is.notUndef, `${meth}: argument fn is undefined`)
}
let context = null
if (is.array(fn)) {
[context, fn] = fn
} else if (fn.fn) {
({ context, fn } = fn)
}
if (context && is.string(fn) && is.func(context[fn])) {
fn = context[fn]
}
if (process.env.NODE_ENV === 'development') {
check(fn, is.func, `${meth}: argument ${fn} is not a function`)
}
return { context, fn, args }
}
export function call(fn, ...args) {
return effect(CALL, getFnCallDesc('call', fn, args))
}
出乎意料都是只返回了一个纯对象( 先不管细节 )。
effect返回的纯对象由于generate函数的机制会将yield的控制权交给外部,用来给generator外层的执行容器task( 这东西我讲不清楚所以就不讲了 )发送一个信号,告诉task该做什么。task在接收到effect发出的指令后将会执行下面这段函数。
function next(arg, isErr) {
// Preventive measure. If we end up here, then there is really something wrong
if (!mainTask.isRunning) {
throw new Error('Trying to resume an already finished generator')
}
try {
let result
if (isErr) {
result = iterator.throw(arg)
} else if (arg === TASK_CANCEL) {
/**
getting TASK_CANCEL automatically cancels the main task
We can get this value here
- By cancelling the parent task manually
- By joining a Cancelled task
**/
mainTask.isCancelled = true
/**
Cancels the current effect; this will propagate the cancellation down to any called tasks
**/
next.cancel()
/**
If this Generator has a `return` method then invokes it
This will jump to the finally block
**/
result = is.func(iterator.return) ? iterator.return(TASK_CANCEL) : { done: true, value: TASK_CANCEL }
} else if (arg === CHANNEL_END) {
// We get CHANNEL_END by taking from a channel that ended using `take` (and not `takem` used to trap
End of channels)
result = is.func(iterator.return) ? iterator.return() : { done: true }
} else {
result = iterator.next(arg)//这里将会执行generator并将结果赋值给result
}
if (!result.done) {//这里会判断这个generator是否执行完毕
runEffect(result.value, parentEffectId, '', next) //这里的runEffect就是各种执行结果的返回(全部流程到此结束)
} else {
/**
This Generator has ended, terminate the main task and notify the fork queue
**/
mainTask.isMainRunning = false
mainTask.cont && mainTask.cont(result.value)
}
} catch (error) {
if (mainTask.isCancelled) {
log('error', `uncaught at ${name}`, error.message)
}
mainTask.isMainRunning = false
mainTask.cont(error, true)
}
}
Redux-saga优点:
Redux异步方案选型:https://zhuanlan.zhihu.com/p/24337401
Redux异步处理之Redux:https://www.dazhuanlan.com/2020/01/17/5e20a402eb6f3/