原始的Redux里面,action creator必须返回plain object,而且必须是同步的。而在实际业务中往往有大量异步场景,定时器,网络请求等等异步操作。可以使用中间件(middleware)改写 dispatch 方法,因此可以更灵活的控制 dispatch 的时机,这对于处理异步场景非常有效。社区常见的中间件有 redux-thunk、
redux-promise、redux-saga、redux-observable 等
// 同步数据
view -> action -> reducer -> store
// 异步数据
view —> action —> middleware —> action(plain) —> reducer —> store
1、redux-thunk
- 作用
- 通过拦截处理函数类型的
action,通过回调来控制触发普通action - 是一个典型的函数式编程,巧用了闭包,让
dispatch方法在函子内没有被销毁
- 通过拦截处理函数类型的
- 缺点:
- action的形式不统一
- 异步操作太为分散、不好维护
组件内使用
// store 对象
const store = createStore(
reducer,
// 此处使用 thunk 时传递了额外参数(见源码)
applyMiddleware(thunk.withExtraArgument({ api, whatever }))
);
// reducer
const reducer = function (oldState, action) {
switch (action.type) {
case FETCH_DATA_START:
// 处理 loading 等
case FETCH_DATA_SUCCESS:
// 更新 store 等处理
case FETCH_DATA_FAILED:
// 提示异常
}
};
// action 中调用
const featData = function (id) {
// 异步数据流
return function (dispatch, getState) {
api.fetchData(id)
.then((response) => {
// 请求成功时 dispatch
dispatch({
type: FETCH_DATA_SUCCESS,
payload: response,
});
})
.catch((error) => {
// 请求失败时 dispatch
dispatch({
type: FETCH_DATA_FAILED,
payload: error,
});
});
};
};
// 组件内调用 dispatch,接受一个函数
store.dispatch(featData(88));
源码分析
// 1、简单版本
const thunk = ({ dispatch, getState }) => (next) => (action) => {
// 判断 action 是否为函数
if (typeof action === "function") {
return action(dispatch, getState);
}
// 否则按照普通 action 处理
return next(action);
};
// 2、参数版本
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;
2、redux-promise
redux-promise 直接将这个 promise 作为 action 给 dispatch
组件内使用
//reducer
const reducer = function (oldState, action) {
switch (action.type) {
case FETCH_DATA:
if (action.status === "success") {
// 更新 store 等处理
} else {
// 提示异常
}
}
};
//action creator
const getData = function (id) {
return {
type: FETCH_DATA,
payload: api.fetchData(id), // 直接将 promise 作为 payload
};
};
源码分析
import isPromise from "is-promise";
import { isFSA } from "flux-standard-action";
const promiseMiddleware = ({ dispatch }) => (next) => (action) => {
// 判断是否是标准的 flux action
if (!isFSA(action)) {
return isPromise(action) ? action.then(dispatch) : next(action);
}
return isPromise(action.payload) ?
action.payload
.then((result) => dispatch({ ...action, payload: result }))
.catch((error) => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
}) :
next(action);
};
3、redux-saga
- 作用
- 统一形式的 action、集中处理异步操作
redux-saga作用是一个监听器,专门监听action- 声明式易测的 Effects,比如无阻塞调用,中断任务
- 保持了 action 的原义,保持 action 的简洁,把所有带副作用的地方独立开来
- 缺点
redux-saga使用的是generator
Effect提供的具体方法
import {take,call,put,select,fork,takeEvery,takeLatest} from 'redux-saga/effects'
createSagaMiddleware(options):创建中间件、链接 Saga 到 Redux 的 store 中
context: Object- saga 的上下文初始值sagaMonitor: 在派发事件时会通知sagaMonitoronError: (error: Error, { sagaStack: string })当 sagas中有未捕获的errors,middleware 会调用这个方法来处理。在追踪错误流时,这个方法很有用effectMiddlewares: Function [] - 这个方法能阻拦任意的 effect,然后将该 effect 传给你自己调用或者传给别的middleware
run:来执行 Sagas,于 applyMiddleware 阶段之后执行 Sagas
take
- 使用拉取
(pull)模式监听 action - 为空或*,那么所有action都会匹配到
阻塞方法遇到action才会向后执行
import { take, call, put } from 'redux-saga/effects'
import Api from '...'
function* authorize(user, password) {
try {
// 调用 authorize 成功、存储 token 、并等待 `LOGOUT` action
const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
return token
} catch(error) {
// 调用 authorize 失败、等待一个新的 `LOGIN_REQUEST` action
yield put({type: 'LOGIN_ERROR', error})
}
}
function* loginFlow() {
// 一旦到达流程最后一步,通过等待一个新的 `LOGIN_REQUEST` action 来启动一个新的迭代
while(true) {
const {user, password} = yield take('LOGIN_REQUEST')
// 1、使用 call 阻塞执行
// 2、当 LOGIN_REQUEST 没有执行完成时、调用 LOGOUT 会报错
const token = yield call(authorize, user, password)
if(token) {
yield call(Api.storeItem({token}))
yield take('LOGOUT')
yield call(Api.clearItem('token'))
}
}
}
fork
- 功能等同于 call、 apply
非阻塞执行
import { isCancelError } from 'redux-saga'
import { fork, call, take, put } from 'redux-saga/effects'
import Api from '...'
function* authorize(user, password) {
try {
const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
} catch(error) {
// 任务被取消
if(!isCancelError(error)){
yield put({type: 'LOGIN_ERROR', error})
}
}
}
function* loginFlow() {
while(true) {
const {user, password} = yield take('LOGIN_REQUEST')
yield fork(authorize, user, password)
yield take(['LOGOUT', 'LOGIN_ERROR'])
// 1、如果 Api 调用过程中,触发了 LOGOUT ACTION
// 2、需要手动取消,否则将有 2 个并发的任务
if(action.type === 'LOGOUT'){
yield cancel(task)
}
yield call(Api.clearItem('token'))
}
}
call、apply
异步请求时使用创建一个纯文本对象描述函数调用阻塞执行- 确保执行函数调用并在响应被 resolve 时恢复 generator
import { call } from 'redux-saga/effects'
function* fetchProducts() {
const products = yield call(Api.fetch, '/products' , arg1, arg2, ...)
// ...
}
// 方便测试用例
assert.deepEqual(
iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts should yield an Effect call(Api.fetch, './products')"
)
put
- 对应
redux 中 dispatch - 创建一个将执行异步 action 的任务、通过 reducer 更改 state
import { call, put } from 'redux-saga/effects'
export function* fetchData(action) {
try {
const data = yield call(Api.fetchUser, action.payload.url);
yield put({type: "FETCH_SUCCEEDED", data});
} catch (error) {
yield put({type: "FETCH_FAILED", error});
}
}
takeEvery
监听 action触发异步任务允许并发即同时处理多个相同的 action- 为空或*,那么所有action都会匹配到
import { takeEvery } from 'redux-saga'
function* watchFetchData() {
// 监听 FETCH_REQUESTED
yield* takeEvery('FETCH_REQUESTED', fetchData)
}
takeLatest
- 捕获每一次匹配 pattern 的 action
不允许并发处理中的 action 会被取消,只会执行当前的- 为空或*,那么所有action都会匹配到
import { takeEvery } from 'redux-saga'
function* watchFetchData() {
// 监听 FETCH_REQUESTED
yield* takeLatest('FETCH_REQUESTED', fetchData)
}
takeLeading
- 一旦任务开始,在任务结束前
takeLeading将不会捕获action - 只会在没有执行任务时,才会监听action
const takeLeading = (patternOrChannel, saga, ...args) =>
fork(function* () {
while (true) {
const action = yield take(patternOrChannel);
yield call(saga, ...args.concat(action));
}
}
);
select:对应的是redux中的getState
const id = yield select(state => state.id);
all
- 并行运行多个Effect
- 等待他们同时完成
// saga模块化引入
import { fork, all } from "redux-saga/effects";
// 异步逻辑
import { loginSagas } from "./login";
// 单一进入点,一次启动所有Saga
export default function* rootSaga() {
yield all([fork(loginSagas)]);
}
错误处理
function* fetchProducts() {
try {
const products = yield call(Api.fetch, '/products')
yield put({ type: 'PRODUCTS_RECEIVED', products })
}
catch(error) {
yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
}
}
3、redux-observable
作用:
redux-observable的核心就是Epics- 收一个
action(plain object) ,返回一个action流
// 安装
yarn add rxjs redux-observable