【问题标题】:Pros/cons of using redux-saga with ES6 generators vs redux-thunk with ES2017 async/await使用 ES6 生成器的 redux-saga 与 ES2017 async/await 的 redux-thunk 的优缺点
【发布时间】:2016-04-28 02:39:16
【问题描述】:

现在有很多关于 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-thunk 和 redux-saga 的答案:stackoverflow.com/a/34623840/82609
  • :: 之前的this.onClick 是做什么的?
  • @ZhenyangHua 它是将函数绑定到对象的简写(this),又名this.onClick = this.onClick.bind(this)。通常建议在构造函数中使用较长的形式,因为在每次渲染时都会重新绑定简写形式。
  • 我明白了。谢谢!我看到人们经常使用bind() 将this 传递给函数,但我现在开始使用() =&gt; method()。
  • @Hosar 我在生产环境中使用了一段时间的 redux 和 redux-saga,但实际上在几个月后迁移到 MobX,因为开销更少

标签: javascript reactjs redux redux-thunk redux-saga


【解决方案1】:

在 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)。即动作不断推送到 thunk,而 thunk 无法控制何时停止处理这些动作。

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

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

  • 处理 LOGOUT 用户操作

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

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

您将如何使用 thunk 来实现它;同时还为整个流程提供完整的测试覆盖?以下是 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后台任务。如果authAndRefreshTokenOnExpiry 在call(authorize, {token}) 调用中被阻塞,它也会被取消。取消自动向下传播。

您可以找到runnable demo of the above flow

【讨论】:

  • @yassine delay 函数来自哪里?啊,找到了:github.com/yelouafi/redux-saga/blob/…
  • redux-thunk 代码可读性强且不言自明。但是redux-sagas 一个真的不可读,主要是因为那些类似动词的功能:call,fork,take,put...
  • @syg,我同意 call、fork、take 和 put 在语义上更友好。然而,正是这些类似动词的函数使所有副作用都可以测试。
  • @syg 仍然是一个带有那些怪异动词的函数,函数比具有深层承诺链的函数更具可读性
  • 那些“奇怪”的动词还可以帮助您概念化 saga 与来自 redux 的消息的关系。您可以从 redux 中取出 消息类型——通常以触发下一次迭代,并且您可以放入新消息以广播副作用的结果。跨度>
【解决方案2】:

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

专业版(使用传奇):

  • 可测试性。测试 sagas 非常容易,因为 call() 返回一个纯对象。测试 thunk 通常需要您在测试中包含一个 mockStore。

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

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

缺点:

  • 生成器语法。

  • 需要学习很多概念。

  • API 稳定性。似乎 redux-saga 仍在添加功能(例如 Channels?)并且社区没有那么大。如果该库有一天会进行非向后兼容的更新,则会令人担忧。

【讨论】:

  • 只是想说点什么,action creator 不一定是纯函数,这点Dan自己已经多次宣称了。
  • 到目前为止,随着使用和社区的扩展,非常推荐redux-sagas。此外,API 也变得更加成熟。考虑删除 API stability 的 Con 作为更新以反映当前情况。
  • saga 的启动次数多于 thunk,并且它的最后一次提交也在 thunk 之后
  • 是的,FWIW redux-saga 现在有 12k 星,redux-thunk 有 8k
  • 我要添加 sagas 的另一个挑战,即 sagas 默认情况下完全与动作和动作创建者分离。虽然 Thunks 直接将动作创建者与其副作用联系起来,但 sagas 使动作创建者与倾听他们的 sagas 完全分离。这具有技术优势,但会使代码更难理解,并且会模糊一些单向概念。
【解决方案3】:

我想根据我的个人经验添加一些 cmets(同时使用 sagas 和 thunk):

Sagas 非常适合测试:

  • 您无需模拟使用效果包装的函数
  • 因此测试干净、易读且易于编写
  • 使用 sagas 时,动作创建者大多返回纯对象字面量。与 thunk 的 Promise 不同,它也更容易测试和断言。

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

  • 等待一个/多个动作被调度 (take)
  • 取消现有例程(cancel、takeLatest、race)
  • 多个例程可以监听同一个动作(take、takeEvery、...)

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

  • channels 监听外部事件源(例如 websockets)
  • 前叉模型 (fork, spawn)
  • 油门
  • ...

Sagas 是伟大而强大的工具。然而,权力伴随着责任。当您的应用程序增长时,您可能很容易迷失方向,因为您可能会弄清楚谁在等待分派动作,或者分派某个动作时发生的一切。另一方面,thunk 更简单,更容易推理。选择一个或另一个取决于许多方面,例如项目的类型和规模、项目必须处理的副作用类型或开发团队的偏好。无论如何,只要让您的应用程序简单且可预测。

【讨论】:

    【解决方案4】:

    2020 年 7 月更新:

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

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

    1. hook + async thunk(hook 让一切都变得非常灵活,因此您实际上可以将异步 thunk 放在您想要的位置并将其用作正常功能,例如,仍然在 action.ts 中编写 thunk,然后 useDispatch() 来触发 thunk:https://stackoverflow.com/a/59991104/5256695 ),
    2. useRequest,
    3. GraphQL/Apollo useQuery useMutation
    4. react-fetching-library
    5. 数据获取/API 调用库、工具、设计模式等的其他流行选择

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

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


    2019 年 3 月的原帖:

    只是一些个人经验:

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

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

    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,这些功能远比 thunk 强大。

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

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

    【讨论】:

    • 我同意你的 2020 更新,我已经使用 saga 1 年了,然后才切换到一个极简的 hook api 库,它可以很好地处理副作用,而不会增加更多的复杂性。如果有兴趣:github.com/marcin-piela/react-fetching-library(我不是这个库的作者)
    • @Jonathan - 您能否提供一个代码示例来说明“hook + async thunk”的含义?
    • 考虑到 React Hooks,Saga 是否仍然“推荐”?如果我在考虑这个论点时持谨慎态度,那对我来说可能是一个关键因素......
    • 就个人而言,我建议在大多数正常情况下使用hook + thunk,但最好还是自己检查sage doc,看看你的项目是否需要它的任何特殊功能。 @BBaysinger
    • 我选择它仅仅是因为它的简单性、可测试性以及它是“推荐的”,但没有其他特别的。对您而言,不将其单独用于这些事情的决定因素是什么?
    【解决方案5】:

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

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

    我见过几个项目,其中 thunk 被视为来自 MVC 模式的控制器,这很快就变成了无法维护的混乱。

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

    【讨论】:

      【解决方案6】:

      Thunks 与 Sagas

      Redux-Thunk 和 Redux-Saga 在几个重要方面有所不同,它们都是 Redux 的中间件库(Redux 中间件是通过 dispatch() 方法拦截进入 store 的操作的代码)。

      一个动作实际上可以是任何东西,但如果您遵循最佳实践,那么一个动作就是一个普通的 javascript 对象,其中包含一个类型字段,以及可选的有效负载、元和错误字段。例如

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

      Redux-Thunk

      除了分派标准动作之外,Redux-Thunk 中间件还允许您分派特殊功能,称为thunks。

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

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

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

      Redux-Saga

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

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

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

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

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

      For more details see this article.

      【讨论】:

        【解决方案7】:

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

        所以,另一个想法可能是使用带有 redux-thunk 的生成器,但对我来说,这似乎是在尝试发明一辆带有方形轮子的自行车。

        当然,生成器更容易测试。

        【讨论】:

          【解决方案8】:

          这是一个结合了 redux-saga 和 redux-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)
              })
            }
          }
          

          【讨论】:

          • 在 React 组件中使用 then() 是违反范式的。您应该在 componentDidUpdate 中处理更改的状态,而不是等待解决承诺。
          • @Maxincredible52 这不适用于服务器端渲染。
          • 根据我的经验,Max 的观点仍然适用于服务器端渲染。这可能应该在路由层的某个地方处理。
          • @Maxincredible52 为什么它违反了范式,你在哪里读到的?我通常与@Diego Haz 类似,但在 componentDidMount 中进行(根据 React 文档,网络调用应该在那里进行)所以我们有 componentDidlMount() { this.props.doSomething().then((detail) =&gt; { this.setState({isReady: true})} }
          【解决方案9】:

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

          来自纪录片

          redux-auto 通过允许您创建一个返回承诺的“动作”函数来解决这个异步问题。伴随您的“默认”功能操作逻辑。

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

          我们的想法是让每个action in a specific file。将文件中的服务器调用与“待处理”、“已完成”和“已拒绝”的减速器功能放在一起。这使得处理 Promise 变得非常容易。

          它还会自动将helper object(called "async") 附加到您的状态原型,允许您在 UI 中跟踪请求的转换。

          【讨论】:

          • 我做了+1,即使它是不相关的答案,因为也应该考虑不同的解决方案
          • 我认为-的存在是因为他没有透露他是该项目的作者
          猜你喜欢
          • 2019-04-20
          • 2019-09-09
          • 2017-05-28
          • 2016-02-26
          • 2021-09-11
          • 1970-01-01
          • 2019-02-11
          • 2018-01-06
          相关资源
          最近更新 更多