将 redux-saga 与 ES6 生成器与 ES2017 async/await 一起使用 redux-thunk

Pros/cons of using redux-saga with ES6 generators vs redux-thunk with ES2017 async/await

本文关键字:await 一起 redux-thunk async ES2017 ES6 redux-saga      更新时间:2023-09-26

现在有很多关于Redux镇最新孩子的讨论,redux-saga/redux-saga。它使用生成器函数来侦听/调度操作。

在我考虑

它之前,我想知道使用 redux-saga 而不是下面我将 redux-thunk 与 async/await 一起使用的方法的优缺点。

组件可能如下所示,像往常一样调度操作。

import { login } from 'redux/auth';
class LoginForm extends Component {
  onClick(e) {
    e.preventDefault();
    const { user, pass } = this.refs;
    this.props.dispatch(login(user.value, pass.value));
  }
  render() {
    return (<div>
        <input type="text" ref="user" />
        <input type="password" ref="pass" />
        <button onClick={::this.onClick}>Sign In</button>
    </div>);
  } 
}
export default connect((state) => ({}))(LoginForm);

然后我的操作看起来像这样:

// auth.js
import request from 'axios';
import { loadUserData } from './user';
// define constants
// define initial state
// export default reducer
export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}
// more actions...

// user.js
import request from 'axios';
// define constants
// define initial state
// export default reducer
export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}
// more actions...

在 redux-saga 中,相当于上面的例子是

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST)
    try {
      let { data } = yield call(request.post, '/login', { user, pass });
      yield fork(loadUserData, data.uid);
      yield put({ type: LOGIN_SUCCESS, data });
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}
export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(request.get, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

首先要注意的是,我们使用 yield call(func, ...args) 形式调用 api 函数。 call 不执行效果,它只是创建一个普通对象,如 {type: 'CALL', func, args} .执行被委托给 redux-saga 中间件,该中间件负责执行函数并恢复生成器及其结果。

主要优点是您可以使用简单的相等性检查在 Redux 之外测试生成器

const iterator = loginSaga()
assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))
// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)
// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN_ERROR, error: mockError })
)

请注意,我们通过简单地将模拟的数据注入迭代器的next方法来模拟 api 调用结果。模拟数据比模拟函数简单得多。

要注意的第二件事是 打电话给yield take(ACTION) .Thunks 由动作创建者在每个新动作上调用(例如 LOGIN_REQUEST)。也就是说,动作不断地被推到投手,而投手无法控制何时停止处理这些动作。

在redux-saga中,生成器拉动下一个动作,即他们可以控制何时监听某个动作,何时不监听。在上面的示例中,流指令放置在while(true)循环中,因此它将侦听每个传入的操作,这在某种程度上模仿了 thunk 推送行为。

拉取方法允许实现复杂的控制流。例如,假设我们要添加以下要求

  • 处理注销用户操作

  • 第一次成功登录后,服务器会返回一个令牌,该令牌会在存储在expires_in字段中的某个延迟中过期。我们必须在每expires_in毫秒内在后台刷新授权

  • 请注意,在等待 api 调用结果(初始登录或刷新)时,用户可能会在两者之间注销。

您将如何通过 thunks 实现这一点;同时为整个流程提供完整的测试覆盖率?以下是Sagas的外观:

function* authorize(credentials) {
  const token = yield call(api.authorize, credentials)
  yield put( login.success(token) )
  return token
}
function* authAndRefreshTokenOnExpiry(name, password) {
  let token = yield call(authorize, {name, password})
  while(true) {
    yield call(delay, token.expires_in)
    token = yield call(authorize, {token})
  }
}
function* watchAuth() {
  while(true) {
    try {
      const {name, password} = yield take(LOGIN_REQUEST)
      yield race([
        take(LOGOUT),
        call(authAndRefreshTokenOnExpiry, name, password)
      ])
      // user logged out, next while iteration will wait for the
      // next LOGIN_REQUEST action
    } catch(error) {
      yield put( login.error(error) )
    }
  }
}

在上面的示例中,我们使用 race 来表达我们的并发要求。如果take(LOGOUT)赢得比赛(即用户点击了注销按钮)。比赛将自动取消authAndRefreshTokenOnExpiry后台任务。如果authAndRefreshTokenOnExpirycall(authorize, {token})通话中被阻止,它也将被取消。取消会自动向下传播。

您可以找到上述流程的可运行演示

除了

库作者相当彻底的答案外,我还将添加我在生产系统中使用 saga 的经验。

专业版(使用传奇):

  • 测试。测试sagas非常容易,因为call()返回了一个纯对象。测试笨蛋通常需要您在测试中包含模拟商店。

  • redux-saga 附带了许多关于任务的有用帮助程序函数。在我看来,saga 的概念是为您的应用程序创建某种后台工作线程/线程,它们充当 react redux 架构中缺失的部分(actionCreators 和 reducer 必须是纯函数。这就引出了下一点。

  • Sagas提供独立的地方来处理所有副作用。根据我的经验,修改和管理通常比 thunk 操作更容易。

缺点:

  • 生成器语法。

  • 很多概念需要学习。

  • API 稳定性。似乎redux-saga仍在添加功能(例如频道?),并且社区没有那么大。如果库有一天进行不向后兼容的更新,则存在问题。

我只想根据我的个人经验添加一些评论(使用 sagas 和 thunk):

传奇非常适合测试:

  • 你不需要模拟用效果包装的函数
  • 因此,测试干净、可读且易于编写
  • 使用 sagas 时,动作创建者大多返回普通对象文字。与 thunk 的承诺不同,它也更容易测试和断言。

传奇更强大。在一个 thunk 的动作创作者中你能做的所有事情你也可以在一个传奇中做,但反之则不然(或者至少不容易)。例如:

  • 等待一个或多个操作被调度 ( take
  • 取消现有例程 ( canceltakeLatestrace
  • 多个例程可以侦听同一个动作(taketakeEvery、...)

Sagas 还提供了其他有用的功能,这些功能概括了一些常见的应用程序模式:

  • channels侦听外部事件源(例如 websockets)
  • 货叉模型 ( forkspawn
  • 节流阀

传奇是伟大而强大的工具。然而,随着权力而来的是责任。当您的应用程序增长时,通过弄清楚谁在等待调度操作,或者调度某些操作时会发生什么,您很容易迷失方向。另一方面,thunk 更简单,更容易推理。选择一个或另一个取决于许多方面,例如项目的类型和大小、项目必须处理的副作用类型或开发团队的偏好。在任何情况下,只要保持您的应用程序简单且可预测。

2020 年 7 月更新:

在过去的 16 个月里,React 社区最显着的变化可能是 React 钩子

根据我的观察,为了更好地与功能组件和钩子兼容,项目(即使是那些大型项目)也会倾向于使用:

    hook + async thunk(
  1. hook 使一切变得非常灵活,所以你实际上可以将 async thunk 放在你想要的地方,并将其用作普通函数,例如,仍然在 action.ts 中编写 thunk,然后使用 Dispatch() 触发 thunk:https://stackoverflow.com/a/59991104/5256695),
  2. 使用请求,
  3. GraphQL/Apollo useQuery useMutation
  4. 反应获取库
  5. 数据获取/API 调用库、工具、设计模式等其他流行选择

相比之下,与上述方法相比,redux-saga目前在大多数正常的 API 调用情况下并没有真正提供显着的好处,同时通过引入许多 saga 文件/生成器来增加项目复杂性(也因为redux-saga的最后一个版本 v1.1.1 是在 2019 年 9 月 18 日,那是很久以前的事了)。

但是,redux-saga仍然提供了一些独特的功能,例如赛车效果和并行请求。因此,如果您需要这些特殊功能,redux-saga仍然是一个不错的选择。

<小时 />

2019年3月的原始帖子:

只是一些个人经验:

  1. 对于编码风格和可读性,过去使用 redux-saga 的最显着优势之一是避免 redux-thunk 中的回调地狱——不再需要使用许多嵌套/catch。但是现在随着 async/await thunk 的流行,在使用 redux-thunk 时也可以以同步方式编写异步代码,这可以被视为 redux-thunk 的改进。

  2. 在使用redux-saga时,可能需要编写更多的样板代码,尤其是在Typescript中。例如,如果要实现一个 fetch 异步函数,数据和错误处理可以直接在一个 thunk 单元中执行.js只需一个 FETCH 操作。但是在 redux-saga 中,可能需要定义FETCH_START、FETCH_SUCCESS和FETCH_FAILURE操作及其所有相关的类型检查,因为 redux-saga 的功能之一是使用这种丰富的"令牌"机制来创建效果并指示 redux 存储以便于测试。当然,人们可以在不使用这些动作的情况下写一个传奇,但这会使它类似于一个笨蛋。

  3. 就文件结构而言,redux-saga在许多情况下似乎更加明确。人们可以很容易地在每个sagas.ts中找到一个与异步相关的代码,但是在redux-thunk中,人们需要在行动中看到它。

  4. 简单测试可能是 redux-saga 中的另一个加权功能。这真的很方便。但需要澄清的一件事是,redux-saga "调用"测试不会在测试中执行实际的 API 调用,因此需要为 API 调用后可能使用的步骤指定示例结果。因此,在编写redux-saga之前,最好详细计划一个saga及其相应的sagas.spec.ts。

  5. Redux-saga还提供了许多高级功能,例如并行运行任务,并发助手,如takeLatest/takeEvery,fork/spawn,它们比thunks强大得多。

总之,就

我个人而言,我想说:在许多正常情况下和中小型应用程序中,请使用 async/await 风格的 redux-thunk。它将为您节省许多样板代码/操作/类型定义,并且您无需切换许多不同的sagas.ts并维护特定的sagas树。但是,如果您正在开发一个具有复杂异步逻辑的大型应用程序,并且需要并发/并行模式等功能,或者对测试和维护有很高的需求(尤其是在测试驱动开发中),redux-sagas 可能会挽救您的生命。

无论如何,redux-saga并不比redux本身更困难和复杂,它没有所谓的陡峭学习曲线,因为它的核心概念和API非常有限。花少量时间学习redux-saga可能会在未来的某一天使自己受益。

根据我的经验,我回顾了几个不同的大型 React/Redux 项目,Sagas 为开发人员提供了一种更结构化的代码编写方式,这种方式更容易测试,也更难出错。

是的,一开始有点奇怪,但大多数开发人员在一天内就对它有了足够的了解。我总是告诉人们不要担心yield一开始做什么,一旦你写了几个测试,它就会来找你。

我看过几个项目,其中笨蛋被当作MVC patten的控制器对待,这很快就会变成一个无法控制的混乱。

我的建议是在需要与单个事件相关的 A 触发器 B 类型内容时使用 Sagas。对于任何可能跨越多个操作的内容,我发现编写自定义中间件并使用 FSA 操作的元属性来触发它更简单。

Thunks vs. Sagas

Redux-ThunkRedux-Saga在一些重要方面有所不同,两者都是 Redux 的中间件库(Redux 中间件是通过 dispatch() 方法拦截进入存储的操作的代码)。

操作

可以是字面上的任何内容,但如果您遵循最佳实践,则操作是一个普通的 JavaScript 对象,其中包含类型字段、可选的有效负载、元和错误字段。

例如
const loginRequest = {
    type: 'LOGIN_REQUEST',
    payload: {
        name: 'admin',
        password: '123',
    }, };

Redux-Thunk

除了调度标准操作外,Redux-Thunk中间件还允许您调度称为thunks的特殊功能。

Thunks(在Redux中)通常具有以下结构:

export const thunkName =
   parameters =>
        (dispatch, getState) => {
            // Your application logic goes here
        };

也就是说,thunk是一个函数,它(可选)接受一些参数并返回另一个函数。内部函数采用dispatch function函数和getState函数 - 两者都将由Redux-Thunk中间件提供。

雷杜克斯-萨加

Redux-Saga中间件允许您将复杂的应用程序逻辑表示为称为 sagas 的纯函数。从测试的角度来看,纯函数是可取的,因为它们是可预测和可重复的,这使得它们相对容易测试。

Sagas是通过称为生成器函数的特殊功能实现的。这些是ES6 JavaScript的新功能。基本上,执行会在您看到收益语句的任何地方跳入和跳出生成器。将 yield 语句视为导致生成器暂停并返回生成的值。稍后,调用方可以在 yield 后面的语句中恢复生成器。

生成器函数是这样定义的函数。请注意函数关键字后面的星号。

function* mySaga() {
    // ...
}

一旦登录传奇注册到Redux-Saga.但是,第一行的yield将暂停传奇,直到将类型为 'LOGIN_REQUEST' 的操作发送到商店。一旦发生这种情况,执行将继续。

有关更多详细信息,请参阅此文章。

一个快速说明。发电机是可取消的,异步/等待 - 不是。因此,对于问题中的一个例子,选择什么并没有真正的意义。但对于更复杂的流程,有时没有比使用发电机更好的解决方案了。

所以,另一个想法可能是使用带有redux-thunk的发电机,但对我来说,这似乎是试图发明一种带有方形轮子的自行车。

当然,发电机更容易测试。

给这个答案一些背景: 嗨,我是 Redux 维护者。

我们最近在 Redux 文档中添加了一个新的副作用方法页面,该页面应该提供有关所有这些的大量信息,但我也会尝试在这里写一些简短的内容,因为这个问题得到了很多曝光。

2022 年,我们将侦听器中间件添加到官方 Redux 工具包中,用于"反应式 Redux 逻辑"。它可以做Sagas可以做的大多数事情(通道除外),不需要生成器语法和更好的TypeScript支持.
不过,这并不意味着您应该使用侦听器中间件编写所有内容 - 我们建议始终尽可能先使用 thunks,并在 thunks 无法执行您想做的事情时使用侦听器中间件。

一般来说,截至 2023 年,我们的立场是,只有当您有其他中间件无法满足的特殊需求时,才应该使用 sagas。(本质上:如果您需要频道。

我们的建议是:

数据获取

  • 使用 RTK 查询作为数据获取和缓存的默认方法
  • 如果 RTKQ 由于某种原因不能完全适合,请使用 createAsyncThunk
  • 只有在其他方法都不起作用的情况下,才退回到手写的笨蛋
  • 不要使用传奇或可观察量来获取数据!

对操作/状态更改做出反应,异步工作流

  • 使用 RTK 侦听器作为响应存储更新和编写长时间运行的异步工作流的默认侦听器
  • 仅当听众不能很好地解决您的用例时才使用sagas/可观察量

具有状态访问的逻辑

  • 将 thunks 用于复杂的同步和适度的异步逻辑,包括访问 getState 和调度多个操作

这是一个结合了redux-sagaredux-thunk的最佳部分(优点)的项目: 您可以处理 Sagas 的所有副作用,同时通过dispatching相应的操作来获得承诺:https://github.com/diegohaz/redux-saga-thunk

class MyComponent extends React.Component {
  componentWillMount() {
    // `doSomething` dispatches an action which is handled by some saga
    this.props.doSomething().then((detail) => {
      console.log('Yaay!', detail)
    }).catch((error) => {
      console.log('Oops!', error)
    })
  }
}

我最近加入了一个大量使用redux-saga的项目,因此也有兴趣了解更多关于传奇方法的好处。

TBH,我还在寻找。读过这篇文章和许多人喜欢它,"专业人士"是难以捉摸的。上面的答案似乎总结为:

  1. 可测试性(忽略实际的 API 调用),
  2. 许多辅助函数,
  3. 熟悉服务器端编码的开发人员。

许多其他说法似乎乐观、误导或完全错误!例如,我见过许多不合理的说法,即"笨蛋不能做X"。但笨蛋是函数。如果一个函数不能做X,那么javascript就不能做X。所以传奇也不能做X。

对我来说,缺点是:

  • 通过使用generator函数混淆关注点。JS 中的生成器返回自定义迭代器。仅此而已。它们没有任何特殊能力来处理异步调用或可取消。任何循环都可以具有突破条件,任何函数都可以处理异步请求,并且任何代码都可以使用自定义迭代器。当人们说这样的话:generators have control when to listen for some actiongenerators are cancellable, but async calls are not时,它通过暗示这些品质是生成器函数所固有的 - 甚至是唯一的 - 来制造混乱。
  • 不明确的用例:AFAIK SAGA 模式用于处理跨服务的并发事务问题。鉴于浏览器是单线程的,很难看出并发性如何带来Promise方法无法处理的问题。顺便说一句:也很难看出为什么这类问题应该在浏览器中处理。
  • 代码可追溯性:通过使用 Redux 中间件将dispatch变成一种事件处理,Sagas 调度操作永远不会到达化简器,因此永远不会被 Redux 工具记录。虽然其他库也这样做,但它通常不必要地复杂,因为浏览器内置了事件处理。间接的优势再次难以捉摸,当直接调用传奇时会更加明显。

如果这篇文章让我看起来对传奇感到沮丧,那是因为我对传奇感到沮丧。他们似乎是一个很好的解决方案,正在寻找要解决的问题。国际 海事 组织。

一种更简单的方法是使用 redux-auto。

来自纪录片

Redux-auto通过允许您创建一个返回承诺的"action"函数来修复此异步问题。伴随您的"默认"函数操作逻辑。

  1. 不需要其他 Redux 异步中间件,例如 thunk、promise-middleware、saga
  2. 轻松允许您将承诺传递到 redux 并为您管理它
  3. 允许您将外部服务呼叫与转换位置放在一起
  4. 将文件命名为"init.js"将在应用启动时调用它一次。这适用于在启动时从服务器加载数据

这个想法是将每个操作放在一个特定的文件中。 将服务器调用与用于"挂起"、"已实现"和"已拒绝"的化简器函数放在文件中。这使得处理承诺变得非常容易。

它还会自动将帮助程序对象(称为"异步")附加到状态的原型,允许您在 UI 中跟踪请求的转换。