Redux 异步数据流方案对比(redux-thunk、redux-promise、redux-saga)

3,241 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情

简介

早期获取异步数据是在 React 组件 componentDidMount 中进行的,通过 callback 或者 promise 的方式再调用 dispatch(action),这样做把 view 层和 model 层混杂在一起,耦合严重,后期维护非常困难。

随着技术的进步,React-Redux异步中间件出现了,它做到了view 层和 model的解耦,使我们的数据流更为清晰。

社区常见的中间件有 redux-thunkredux-promiseredux-saga,今天我们对比分析下这三个异步中间件。

数据流

相较同步数据流,异步数据流有一个异步请求的操作,等异步请求有了结果才会触发action进入到reducer,修改store中的state

同步数据流过程

image.png

异步数据流过程

image.png

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方法,进行异步请求,当请求返回结果后再触发同步actiongetTodoByIdAction进入到相应reducer更新state

每个异步操作相当于需要两个action,一个进行异步请求一个触发reducer更新state

redux-promise

redux-promise相对redux-thunk更简单。

redux-promise不需要两个action,而是一个action。它直接把异步操作作为actionpayload,当异步任务有了结果后自动再触发该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再触发同步actiongetTodoByIdPromiseAction,进入到相应reducer更新state

这样下来比 redux-thunk 的写法简单了不少。

redux-saga

redux-saga相较前两种异步方案有了很大的改动,它不再把异步操作杂糅在action当中,而是单独抽取出来。

单独抽取出来的action作为saga文件。

我们来看看redux-sagaReact中的具体应用。

安装

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文件的整体逻辑是

  1. 当我们sagaMiddleware.run(rootSaga)后会监听我们所有saga文件里面暴露的方法。

  2. 当我们在React组件中,提交了相同typeaction后会被拦截,进入到saga对应的方法。

  3. saga运行我们配置好的方法进行异步操作,并把结果返回。

  4. 然后提交普通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方法中,我们dispatchaction会被自动作为参数。然后在该方法里使用call进行异步请求,当异步任务返回了结果后使用put提交同步action触发reducer更新state

本文对saga只是做了简单的介绍,只使用了几个简单的API,其实saga还有更多强大的用法,可以自行参考saga 中文文档进行学习。

总结

  1. redux-thunk使用简单,直接在action里面进行异步操作,虽然简单但是在action里面写异步逻辑感觉有点混乱。

  2. redux-promise隐藏了异步操作的具体细节,并且只需要一个action就能完成异步操作,相对redux-thunk来说更加简单。

  3. redux-saga,把异步操作单独分离出来放在saga文件中。当我们提交普通action的时候,如果匹配到了saga文件中的监听器就会被拦截下来,然后调用saga里配置的方法进行异步操作。如果没匹配上就走提交普通action的逻辑。总体来说逻辑较为清晰,但是使用成本增加。

这三种异步数据流方案都是React-Redux的最佳实践,这三者没有孰强孰弱,都有各自的应用场景,我们在开发的时候可以根据自身项目实际情况选择使用。

系列文章

React-Router6路由新特性(React-Router4/5和React-Router6对比总结)

对比React-Redux看看Redux Toolkit有哪些优点

后记

感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!