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

时间:2016-01-21 17:45:13

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

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

在我绕过它之前,我想知道使用redux-saga而不是下面我使用redux-thunk和async / await的方法的优点/缺点。 / p>

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

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...

9 个答案:

答案 0 :(得分:417)

在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)。动作创建者会在每个新动作上调用Thunk(例如LOGIN_REQUEST)。即动作不断被推送到thunks,并且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后台任务。如果authAndRefreshTokenOnExpirycall(authorize, {token})来电中被阻止,它也会被取消。取消自动向下传播。

您可以找到runnable demo of the above flow

答案 1 :(得分:83)

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

Pro(使用传奇):

  • 可测试性。由于call()返回一个纯对象,因此测试传奇非常容易。测试thunk通常需要在测试中包含mockStore。

  • redux-saga附带了许多有关任务的有用辅助函数。在我看来,saga的概念是为你的app创建某种后台工作者/线程,它在react redux体系结构中扮演一个缺失的部分(actionCreators和reducers必须是纯函数。)这导致了下一点。 / p>

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

缺点:

  • 生成器语法。

  • 需要学习很多概念。

  • API稳定性。似乎redux-saga仍在添加功能(例如频道?),社区不是那么大。如果图书馆有一天会进行非向后兼容的更新,则会有一个问题。

答案 2 :(得分:22)

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

Sagas非常适合测试:

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

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

  • 等待调度操作/操作(take
  • 取消现有例程(canceltakeLatestrace
  • 多个例程可以收听相同的操作(taketakeEvery,...)

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

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

答案 3 :(得分:2)

暴徒对阵萨加斯

Redux-ThunkRedux-Saga在一些重要方面有所不同,它们都是Redux的中间件库(Redux中间件是用于拦截通过dispatch()方法进入商店的动作的代码)。

一个动作实际上可以是任何东西,但是如果您遵循最佳实践,那么一个动作就是一个普通的javascript对象,它带有一个type字段,以及可选的有效负载,meta和error字段。例如

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的操作将暂停传奇,直到将类型为'LOGIN_REQUEST'的操作分派到商店。一旦发生这种情况,执行将继续。

For more details see this article

答案 4 :(得分:0)

这是一个结合了redux-sagaredux-thunk的最佳部分(专业)的项目:你可以通过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)
    })
  }
}

答案 5 :(得分:0)

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

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

我已经看过几个项目,其中thunk被视为来自MVC patten的控制器,这很快变成了一个不可控制的混乱。

我的建议是在你需要的地方使用Sagas A触发B类与单个事件有关的东西。对于任何可能涉及多个操作的内容,我发现编写客户中间件并使用FSA操作的元属性来触发它更简单。

答案 6 :(得分:0)

只是一些个人经验:

  1. 在编码风格和可读性方面,过去使用redux-saga的最重要优势之一是避免在redux-thunk中使用回调地狱-不再需要使用多个嵌套然后/捕获。但是现在随着async / await thunk的流行,人们也可以在使用redux-thunk时以同步方式编写异步代码,这可能被认为是re​​dux-think的一种改进。

  2. 使用redux-saga时可能需要编写更多样板代码,尤其是在Typescript中。例如,如果要实现获取异步功能,则可以通过一个FETCH动作直接在action.js中的一个thunk单元中执行数据和错误处理。但是在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),它们比thunk更强大。

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

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

答案 7 :(得分:-1)

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

来自documantasion的

  

redux-auto简单地通过允许您创建一个返回promise的“action”函数来修复此异步问题。伴随你的“默认”功能动作逻辑。

  1. 不需要其他Redux异步中间件。例如thunk,promise-middleware,saga
  2. 轻松允许您将承诺传递给redux 并让它为您管理
  3. 允许您将外部服务呼叫与其转换位置共同定位
  4. 命名文件“init.js”将在应用启动时调用一次。这适用于从服务器开始加载数据
  5. 这个想法是让每个action in a specific file。将文件中的服务器调用与“pending”,“fulfilled”和“rejected”的reducer函数共同定位。这使得处理承诺非常容易。

    它还会自动将helper object(called "async")附加到您所在州的原型,允许您在用户界面中跟踪请求的转换。

答案 8 :(得分:-1)

一个快速说明。生成器是可取消的,async / await - 不是。 因此,对于问题的一个例子,它并没有真正理解要选择什么。 但是对于更复杂的流程,有时没有比使用生成器更好的解决方案。

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

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