使用带有 ES6 生成器的 redux-saga 与带有 ES2017 async/await 的 redux-thunk 的优缺点

IT技术 javascript reactjs redux redux-thunk redux-saga
2021-03-06 10:23:50

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

在我考虑它之前,我想知道使用redux-saga而不是下面我使用redux-thunkasync/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...
6个回答

在 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 });
  }
}

首先要注意的是,我们正在使用表单调用 api 函数yield call(func, ...args)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). 动作创建者对每个新动作(例如LOGIN_REQUEST调用 Thunk 即动作不断地被推送给 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})它也会被取消。取消自动向下传播。

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

@yassinedelay函数来自哪里?啊,找到了:github.com/yelouafi/redux-saga/blob/...
2021-04-21 10:23:50
redux-thunk代码非常易读且不言自明。但是redux-sagas一个真的不可读,主要是因为那些类似动词的功能:call, fork, take, put...
2021-04-28 10:23:50
这些“奇怪”的动词还可以帮助您概念化 saga 与来自 redux 的消息的关系。您可以从 redux 中取出消息类型——通常会触发下一次迭代,并且您可以新消息回去以广播副作用的结果。
2021-05-06 10:23:50
@syg 仍然是一个带有那些奇怪动词的函数,它比带有深度Promise链的函数更具可读性
2021-05-15 10:23:50
@syg,我同意 call、fork、take 和 put 在语义上更友好。然而,正是那些类似动词的功能使所有副作用都可以测试。
2021-05-16 10:23:50

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

亲(使用传奇):

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

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

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

骗局:

  • 生成器语法。

  • 很多概念要学习。

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

到目前为止,随着使用和社区的扩大,非常推荐 redux-sagas。此外,API 也变得更加成熟。考虑删除 Con forAPI stability作为更新以反映当前情况。
2021-04-23 10:23:50
我要添加 sagas 的另一个挑战,即默认情况下,sagas与动作和动作创建者完全分离。虽然 Thunk 直接将动作创作者与其副作用联系起来,但传奇让动作创作者与倾听他们的传奇完全分开。这具有技术优势,但会使代码更难以遵循,并且会模糊一些单向概念。
2021-05-07 10:23:50
saga 的开始次数比 thunk 多,它的最后一次提交也是在 thunk 之后
2021-05-13 10:23:50
只是想发表一些评论,动作创建者不必是纯函数,这已被丹本人多次声称。
2021-05-19 10:23:50
是的,FWIW redux-saga 现在有 12k 星,redux-thunk 有 8k
2021-05-19 10:23:50

我只想从我的个人经验中添加一些评论(同时使用 sagas 和 thunk):

Sagas 非常适合测试:

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

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

  • 等待一个或多个动作被分派 ( take)
  • 取消现有例程 ( cancel, takeLatest, race)
  • 多个例程可以侦听相同的动作 ( take, takeEvery, ...)

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

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

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

2020 年 7 月更新:

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

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

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

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

但是,仍然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 async 功能,数据和错误处理可以直接在 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 可能在未来的某一天让自己受益。

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

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

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

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

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