在ES6生成器上使用redux-saga与在ES2017 async / await中使用redux-thunk的优缺点


488

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

在开始思考之前,我想知道使用优缺点的方法,redux-saga而不是下面使用redux-thunk异步/等待方法的方法。

组件可能看起来像这样,像往常一样调度动作。

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-thunk与redux-saga进行比较:stackoverflow.com/a/34623840/82609
Sebastien Lorber

22
什么是::你以前this.onClick做什么?
Downhillski

37
@ZhenyangHua是将函数绑定到对象(this)的简写形式this.onClick = this.onClick.bind(this)。通常建议在构造函数中使用较长的形式,因为速记形式会在每个渲染器上重新绑定。
hampusohlsson

7
我知道了。谢谢!我看到人们花bind()了很多时间来传递this给该函数,但是我() => method()现在开始使用。
Downhillski

2
@Hosar我在生产中使用redux和redux-saga了一段时间,但实际上由于几个月的开销减少而迁移到MobX
hampusohlsson

Answers:


461

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

首先要注意的是,我们正在使用form调用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 。即动作不断地被推向重击,重击无法控制何时停止处理这些动作。

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

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

  • 处理注销用户操作

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

  • 考虑到在等待api调用的结果(初始登录或刷新)时,用户可能会在两次登录之间注销。

您将如何以笨拙的方式实现这一目标?同时为整个流程提供完整的测试覆盖范围?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})它也会被取消。取消会自动向下传播。

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


@yassine delay函数从哪里来?啊,发现:github.com/yelouafi/redux-saga/blob/...
philk

122
redux-thunk代码具有很好的可读性和解释性。但是,redux-sagas一个确实是不可读的,主要是因为这些动词类功能:callforktakeput...
SYG

11
@syg,我同意call,fork,take和put在语义上可以更加友好。但是,正是那些动词式功能使所有副作用都可以测试。
Downhillski

3
@syg仍然具有那些怪异的动词函数,比具有深层promise链的函数更具可读性
Yasser Sinjab

3
这些“怪异”的动词还可以帮助您概念化传奇故事与从redux发出的消息的关系。你可以采取的消息类型进行终极版-往往触发下一次迭代,你可以新的消息早在播出你的副作用的结果。
worc

104

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

专业版(使用传奇):

  • 可测试性。测试call的返回值很简单,因为call()返回一个纯对象。测试thunk通常需要您在测试中包括一个模拟存储。

  • redux-saga附带了许多有用的关于任务的帮助器功能。在我看来,saga的概念是为您的应用程序创建某种后台工作程序/线程,这是React Redux架构中缺少的部分(actionCreators和reducers必须是纯函数。)这引出了下一步。

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

缺点:

  • 生成器语法。

  • 有很多概念需要学习。

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


9
只是想发表评论,动作创建者不一定要是纯粹的功能,而丹本人多次宣称这一功能。
Marson Mao,

14
截至目前,由于其用法和社区的扩展,非常推荐使用redux-sagas。而且,API已经变得更加成熟。考虑删除Con API stability作为更新以反映当前情况。
Denialos

1
saga的启动次数超过了thunk,其最后一次提交也是在thunk之后
amorenew

2
是的,FWIW redux-saga现在拥有12,000星,redux-thunk具有8k
Brian Burns

3
我要补充一下Sagas的另一个挑战,即默认情况下Sagas与动作和动作创建者完全脱钩。尽管Thunks直接将动作创建者与其副作用联系在一起,但“魔鬼传奇”却使动作创作者与聆听他们的魔鬼传奇完全分开。这具有技术上的优势,但会使代码难以遵循,并使某些单向概念变得模糊。
theaceofthespade

33

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

Sagas非常适合测试:

  • 您不需要模拟包装了效果的函数
  • 因此,测试是干净,易读且易于编写的
  • 使用sagas时,动作创建者通常会返回普通对象文字。与thunk的承诺不同,测试和声明也更容易。

Sagas更强大。您可以在一个怪人的动作创建者中做的所有事情,也可以在一个传奇中做,但反之则不行(或者至少不容易)。例如:

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

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

  • channels 侦听外部事件源(例如,websocket)
  • 货叉模型(forkspawn
  • 油门
  • ...

Sagas是强大的工具。然而,权力伴随着责任。随着应用程序的增长,弄清楚谁在等待派发该动作,或者在某个动作被派发后会发生什么,您很容易迷失方向。另一方面,重击更容易推断。选择一个或另一个取决于许多方面,例如项目的类型和大小,项目必须处理的副作用类型或开发团队的偏好。无论如何,只要保持您的应用程序简单和可预测即可。


8

只是一些个人经验:

  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强大。

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

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


5

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

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

我已经看到了几个项目,这些项目已经将thunk视为来自MVC patten的控制器,并且很快就变得混乱不堪。

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


2

暴徒vs萨加斯

Redux-Thunk并且Redux-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
        };

也就是说,a thunk是(可选)采用一些参数并返回另一个函数的函数。内部函数带有dispatch functiongetState函数-两者都将由Redux-Thunk中间件提供。

Redux-Saga

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

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

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

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

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

有关更多详细信息,请参见本文


1

快速说明。生成器是可取消的,异步/等待-不是。因此,对于这个问题的一个例子,它实际上没有意义。但是对于更复杂的流程,有时没有比使用生成器更好的解决方案了。

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

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


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

1
then()在React组件内部使用违反了范式。您应该处理更改后的状态,componentDidUpdate而不要等待承诺被解决。

3
@ Maxincredible52对于服务器端渲染而言并非如此。
Diego Haz

以我的经验,Max对于服务器端渲染仍然是正确的。这可能应该在路由层中的某处进行处理。
ThinkingInBits

3
@ Maxincredible52为什么反对范式,您在哪里读到的?我通常会做类似@Diego Haz的事情,但是要在componentDidMount中做(根据React文档,最好在那儿进行网络调用),所以我们有componentDidlMount() { this.props.doSomething().then((detail) => { this.setState({isReady: true})} }
user3711421

0

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

从文件

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

  1. 无需其他Redux异步中间件。例如,笨拙,承诺中间件,传奇
  2. 轻松地让您将承诺传递给redux 并为您进行管理
  3. 使您可以将外部服务呼叫与它们将转换的位置共存
  4. 命名文件“ init.js”将在应用启动时调用一次。这对于在启动时从服务器加载数据很有用

这个想法是将每个动作都放在一个特定的文件中。将服务器调用与reducer函数一起定位在文件中,以“挂起”,“完成”和“拒绝”。这使得兑现承诺非常容易。

它还会自动将助手对象(称为“异步”)附加到状态原型,从而使您可以在UI中跟踪请求的转换。


2
我做出+1甚至是无关紧要的答案,因为也应该考虑使用不同的解决方案
amorenew

12
我认为-是存在的,因为他没有透露自己是该项目的作者
jreptak
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.