持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情
简介
早期获取异步数据是在 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";
import { createStore, applyMiddleware, compose } from "redux";
import reducers from "./reducers";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducers,
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-promise不需要两个action,而是一个action。它直接把异步操作作为action的payload,当异步任务有了结果后自动再触发该action,进入到reducer更新state。
安装
npm i redux-promise
配置
以中间件形式配置
import promiseMiddleware from "redux-promise";
import { createStore, applyMiddleware, compose } from "redux";
import reducers from "./reducers";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducers,
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 { createStore, applyMiddleware, compose } from "redux";
import createSagaMiddleware from "redux-saga";
import rootSaga from "./sagas/index";
import reducers from "./reducers";
const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducers,
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有哪些优点
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!