简介
早期获取异步数据是在 React
组件 componentDidMount
中进行的,通过 callback
或者 promise
的方式再调用 dispatch(action)
,这样做把 view
层和 model
层混杂在一起,耦合严重,后期维护非常困难。
随着技术的进步,React-Redux
异步中间件出现了,它做到了view
层和 model
的解耦,使我们的数据流更为清晰。
社区常见的中间件有 redux-thunk
、redux-promise
、redux-saga
,今天我们对比分析下这三个异步中间件。
数据流
相较同步数据流,异步数据流有一个异步请求的操作,等异步请求有了结果才会触发action
进入到reducer
,修改store
中的state
。
同步数据流过程
异步数据流过程
redux-thunk
redux-thunk是非常简单的异步处理方案。
我们知道,普通的action只能返回对象。但是使用redux-thunk
后,我们的action
不但可以返回普通对象,还可以返回一个带dispatch
参数的函数,在该函数里就能进行异步请求。
安装
npm i redux-thunk
复制代码
配置
以中间件形式配置
import thunk from "redux-thunk";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
redicers,
composeEnhancers(applyMiddleware(thunk))
);
复制代码
使用
redux-thunk
的整体逻辑就是在actions
中,我们先创建ThunkAction
,在该action
中,我们返回一个函数,函数里面进行异步请求,当异步请求有了结果,我们再触发同步action
,设置最新的state
。
import { getTodoByIdType } from "../types";
// 请求API
const getTodoById = async (payload) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${payload}`
);
const response = await res.json();
return response;
};
// 同步action,设置state
export const getTodoByIdAction = (payload) => {
return {
type: getTodoByIdType,
payload,
};
};
// 异步请求,有了结果再触发同步action设置state
export const getTodoByIdThunkAction = (payload) => {
return async (dispatch) => {
const response = await getTodoById(payload);
dispatch(getTodoByIdAction(response));
};
};
复制代码
上面的例子,当我们在React
组件dispatch(getTodoByIdThunkAction(1))
后会进入到getTodoByIdThunkAction
方法,进行异步请求,当请求返回结果后再触发同步action
即getTodoByIdAction
进入到相应reducer
更新state
。
每个异步操作相当于需要两个action
,一个进行异步请求一个触发reducer
更新state
。
redux-promise
redux-promise相对redux-thunk
更简单。
redux-thunk
不需要两个action
,而是一个action
。它直接把异步操作作为action
的payload
,当异步任务有了结果后自动再触发该action
,进入到reducer
更新state
。
安装
npm i redux-promise
复制代码
配置
以中间件形式配置
import promiseMiddleware from "redux-promise";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
redicers,
composeEnhancers(applyMiddleware(promiseMiddleware))
);
复制代码
使用
import { getTodoByIdType } from "../types";
// 请求API
const getTodoById = async (payload) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${payload}`
);
const response = await res.json();
return response;
};
// 直接将异步请求设置成payload
// 当异步有了结果会自动触发该action,然后进入到reducer更新state
export const getTodoByIdPromiseAction = (payload) => {
return {
type: getTodoByIdType,
payload: getTodoById(payload),
};
};
复制代码
上面的例子,当我们在React
组件dispatch(getTodoByIdPromiseAction(1))
后会进入到getTodoByIdPromiseAction
方法进行异步请求,当请求返回结果后把异步结果作为payload
再触发同步action
即getTodoByIdPromiseAction
,进入到相应reducer
更新state
。
这样下来比 redux-thunk
的写法简单了不少。
redux-saga
redux-saga相较前两种异步方案有了很大的改动,它不再把异步操作杂糅在action
当中,而是单独抽取出来。
单独抽取出来的action
作为saga
文件。
我们来看看redux-saga
在React
中的具体应用。
安装
npm i redux-saga
复制代码
配置
因为redux-saga
单独把异步任务抽取出来了,所以需要有单独的saga
文件来处理异步任务。我们创建一个sagas
文件夹,里面存放saga
文件。
rootSaga
用来组合所有的saga
文件,类似combineReducers
// sagas/index.js
import { all } from "redux-saga/effects";
import saga1 from "./saga1";
import saga2 from "./saga2";
function* rootSaga() {
// 多个saga
yield all([...saga1, ...saga2]);
}
export default rootSaga;
复制代码
以中间件形式配置使用,并启动saga
。
import createSagaMiddleware from "redux-saga";
import rootSaga from "./sagas/index";
const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
redicers,
composeEnhancers(applyMiddleware(sagaMiddleware))
);
// 启动saga,相当于监听
sagaMiddleware.run(rootSaga);
复制代码
使用
saga
文件的整体逻辑是
-
当我们
sagaMiddleware.run(rootSaga)
后会监听我们所有saga
文件里面暴露的方法。 -
当我们在
React
组件中,提交了相同type
的action
后会被拦截,进入到saga
对应的方法。 -
saga
运行我们配置好的方法进行异步操作,并把结果返回。 -
然后提交普通
action
,触发reducer
更新state
。
import { call, takeEvery, takeLatest, put, select } from "redux-saga/effects";
import { getTodoByIdType, fetchTodoByIdType } from "../types";
// 异步接口
const getTodoById = async (payload) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${payload}`
);
const response = await res.json();
return response;
};
// dispatch 的 action 会被作为参数自动接收
function* fetchTodo(action) {
try {
// 调用接口方法
const todo = yield call(getTodoById, action.payload);
console.log("todo", todo);
// 获取state
let item1 = yield select((state) => state.todo.item);
console.log(item1);
// 提交 action
yield put({ type: getTodoByIdType, payload: todo });
// 获取state
let item2 = yield select((state) => state.todo.item);
console.log(item2);
} catch (e) {
// 请求错误置空处理
yield put({ type: getTodoByIdType, payload: {} });
}
}
// 监听 我们 fetchTodoByIdType的action,当监听后会自动执行fetchTodo方法
function* listenGetTodo() {
yield takeEvery(fetchTodoByIdType, fetchTodo); // 始终监听
// yield takeLatest(fetchTodoByIdType, fetchuserr); // 监听最新的一次
}
// 使用数组导出,当有多个方法需要监听的时候放在数组即可
const saga1 = [listenGetTodo()];
export default saga1;
复制代码
上面的例子,当我们在React
组件提交dispatch({type: fetchTodoByIdType, payload: 1})
后会被listenGetTodo
方法拦截。然后触发fetchTodo
方法。在fetchTodo
方法中,我们dispatch
的action
会被自动作为参数。然后在该方法里使用call
进行异步请求,当异步任务返回了结果后使用put
提交同步action
触发reducer
更新state
。
本文对saga
只是做了简单的介绍,只使用了几个简单的API
,其实saga
还有更多强大的用法,可以自行参考saga 中文文档进行学习。
总结
-
redux-thunk
使用简单,直接在action
里面进行异步操作,虽然简单但是在action
里面写异步逻辑感觉有点混乱。 -
redux-promise
隐藏了异步操作的具体细节,并且只需要一个action
就能完成异步操作,相对redux-thunk
来说更加简单。 -
redux-saga
,把异步操作单独分离出来放在saga
文件中。当我们提交普通action
的时候,如果匹配到了saga
文件中的监听器就会被拦截下来,然后调用saga
里配置的方法进行异步操作。如果没匹配上就走提交普通action
的逻辑。总体来说逻辑较为清晰,但是使用成本增加。
这三种异步数据流方案都是React-Redux
的最佳实践,这三者没有孰强孰弱,都有各自的应用场景,我们在开发的时候可以根据自身项目实际情况选择使用。
系列文章
React-Router6路由新特性(React-Router4/5和React-Router6对比总结)
对比React-Redux看看Redux Toolkit有哪些优点
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!