使用带有ES6发生器的redux-saga与带有ES2017异步/等待的redux-thunk的优点/缺点

ham*_*son 449 javascript reactjs redux redux-thunk redux-saga

现在有很多关于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);
Run Code Online (Sandbox Code Playgroud)

然后我的行为看起来像这样:

// 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...
Run Code Online (Sandbox Code Playgroud)
// 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...
Run Code Online (Sandbox Code Playgroud)

Yas*_*afi 437

在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 });
  }
}
Run Code Online (Sandbox Code Playgroud)

首先要注意的是我们使用表单调用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 })
)
Run Code Online (Sandbox Code Playgroud)

注意我们只是通过将模拟数据注入next迭代器的方法来模拟api调用结果.模拟数据比模拟函数更简单.

要注意的第二件事是打电话给yield take(ACTION).动作创建者在每个新动作(例如LOGIN_REQUEST)上调用Thunk .即动作不断被推到thunk,并且thunk无法控制何时停止处理这些动作.

在终极版,传奇,发电机的下一个动作.也就是说,他们可以控制什么时候听某些动作,什么时候不听.在上面的示例中,流指令被放置在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) )
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • `redux-thunk`代码非常易读和自我解释.但是`redux-sagas`一个是不可读的,主要是因为那些类似动词的函数:`call`,`fork`,`take`,`put` ... (107认同)
  • @syg,我同意call,fork,take和put可以更加语义友好.然而,正是这些类似动词的函数使得所有副作用都可以测试. (9认同)
  • 这些“奇怪”的动词还可以帮助您概念化 saga 与来自 redux 的消息的关系。您可以*从redux中取出*消息类型——通常会触发下一次迭代,并且您可以*放回*新消息以广播副作用的结果。 (7认同)
  • @syg仍然是一个函数,那些奇怪的动词函数比具有深度承诺链的函数更具可读性 (2认同)

yjc*_*y12 89

除了图书馆作者的相当全面的答案之外,我将在生产系统中添加使用saga的经验.

Pro(使用传奇):

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

  • redux-saga附带了许多有关任务的有用辅助函数.在我看来,那传奇的概念是创造某种背景工人/线程您的应用程式,在反应Redux的架构(actionCreators和减速器必须是纯功能).这会导致下一个点充当一个缺失.

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

缺点:

  • 生成器语法.

  • 要学习很多概念.

  • API稳定性.似乎redux-saga仍在添加功能(例如频道?),社区不是那么大.如果库有一天会进行非向后兼容的更新,则会引起关注.

  • 截至目前,非常推荐使用redux-sagas作为用途并且社区已经扩展.此外,API已经变得更加成熟.考虑删除Con for"API stability"作为更新以反映当前情况. (11认同)
  • 只是想做一些评论,动作创作者不需要是纯粹的功能,这已经被Dan自己多次声称. (9认同)
  • 是的,FWIW redux-saga现在有12k星,redux-thunk有8k (2认同)
  • 我将添加Sagas的另一个挑战,即默认情况下Sagas *完全*与动作和动作创建者分离。尽管Thunks直接将动作创建者与他们的副作用联系起来,但“魔鬼”将动作创造者与听他们的魔鬼完全分开。这具有技术优势,但会使代码难以遵循,并且可能使某些单向概念变得模糊。 (2认同)

Jon*_*han 29

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. 反应库
  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 的时候也可以写出同步风格的 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 可能在未来的某一天让自己受益。


mad*_*ox2 26

我只想从我的个人经历中添加一些评论(使用传奇和thunk):

Sagas非常适合测试:

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

Sagas更强大.您可以在一个thunk的动作创建者中执行所有操作,您也可以在一个传奇中执行,但反之亦然(或者至少不容易).例如:

  • 等待调度动作/动作(take)
  • 取消现有例程(cancel,takeLatest,race)
  • 多个例程可以听同样的行动(take,takeEvery,...)

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

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

Sagas是伟大而强大的工具.然而,权力来自责任.当您的应用程序增长时,您可以通过确定谁正在等待调度操作,或者在调度某些操作时发生的一切情况而轻易丢失.另一方面,thunk更简单,更容易推理.选择一个或另一个取决于许多方面,如项目的类型和大小,项目必须处理的副作用类型或开发团队偏好.在任何情况下,只需保持您的应用程序简单和可预测.


Dav*_*haw 9

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

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

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

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


Mse*_*Ali 5

Thunks 与 Sagas

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

操作实际上可以是任何内容,但如果您遵循最佳实践,操作就是带有类型字段以及可选负载、元和错误字段的纯 JavaScript 对象。例如

const loginRequest = {
    type: 'LOGIN_REQUEST',
    payload: {
        name: 'admin',
        password: '123',
    }, };
Run Code Online (Sandbox Code Playgroud)

Redux-Thunk

除了调度标准操作之外,Redux-Thunk中间件还允许您调度特殊函数,称为thunks.

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

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

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

Redux-Saga

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

Sagas 是通过称为生成器函数的特殊函数来实现的。这些都是 的新功能ES6 JavaScript。基本上,只要你看到yield 语句,执行就会跳进跳出生成器。将yield语句视为导致生成器暂停并返回生成值的语句。稍后,调用者可以在yield.

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

function* mySaga() {
    // ...
}
Run Code Online (Sandbox Code Playgroud)

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

有关更多详细信息,请参阅这篇文章