使用带有ES6发生器的redux-saga与带有ES2017异步/等待的redux-thunk的优点/缺点 [英] Pros/cons of using redux-saga with ES6 generators vs redux-thunk with ES2017 async/await

查看:126
本文介绍了使用带有ES6发生器的redux-saga与带有ES2017异步/等待的redux-thunk的优点/缺点的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

There is a lot of talk about the latest kid in redux town right now, redux-saga/redux-saga. It uses generator functions for listening to/dispatching actions.

在我绕过它之前,我想知道使用 redux-saga 而不是我正在使用async / await使用 redux-thunk 的方法。

Before I wrap my head around it, I would like to know the pros/cons of using redux-saga instead of the approach below where I'm using redux-thunk with async/await.

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

A component might look like this, dispatch actions like usual.

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

然后我的行为如下所示:

Then my actions look something like this:

// 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-saga中,相当于上面的例子将是

In redux-saga, the equivalent of the above example would be

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调用(func,... args)形式的api函数。 调用不执行效果,它只是创建一个普通的对象,如 {type:'CALL',func,args} 。执行被委托给redux-saga中间件,后者负责执行该函数并使用其结果恢复生成器。

The first thing to notice is that we're calling the api functions using the form yield call(func, ...args). call doesn't execute the effect, it just creates a plain object like {type: 'CALL', func, args}. The execution is delegated to the redux-saga middleware which takes care of executing the function and resuming the generator with its result.

主要优点是你可以测试生成器在Redux之外使用简单的相等检查

The main advantage is that you can test the generator outside of Redux using simple equality checks

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

注意我们只是通过将模拟数据注入<$来模拟api调用结果c $ c>下一个迭代器的方法。模拟数据比模拟函数更简单。

Note we're mocking the api call result by simply injecting the mocked data into the next method of the iterator. Mocking data is way simpler than mocking functions.

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

The second thing to notice is the call to yield take(ACTION). Thunks are called by the action creator on each new action (e.g. LOGIN_REQUEST). i.e. actions are continually pushed to thunks, and thunks have no control on when to stop handling those actions.

在redux-saga中,生成器下一个动作。即他们有权控制何时采取某些行动,何时不采取行动。在上面的示例中,流指令放在 while(true)循环中,因此它将侦听每个传入的操作,这有点模仿thunk推送行为。

In redux-saga, generators pull the next action. i.e. they have control when to listen for some action, and when to not. In the above example the flow instructions are placed inside a while(true) loop, so it'll listen for each incoming action, which somewhat mimics the thunk pushing behavior.

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

The pull approach allows implementing complex control flows. Suppose for example we want to add the following requirements


  • 处理LOGOUT用户操作

  • Handle LOGOUT user action

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

upon the first successful login, the server returns a token which expires in some delay stored in a expires_in field. We'll have to refresh the authorization in the background on each expires_in milliseconds

考虑到在等待api调用的结果(初始登录或刷新)时,用户可以在中间注销。

Take into account that when waiting for the result of api calls (either initial login or refresh) the user may logout in-between.

你如何用thunk实现它;同时还为整个流程提供全面的测试覆盖?以下是Sagas的外观:

How would you implement that with thunks; while also providing full test coverage for the entire flow? Here is how it may look with 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) )
    }
  }
}

在上面的例子中,我们使用种族来表达我们的并发性要求。如果 take(LOGOUT)赢得比赛(即用户点击了一个Logout按钮)。比赛将自动取消 authAndRefreshTokenOnExpiry 后台任务。如果 authAndRefreshTokenOnExpiry 调用(authorize,{token})中间被阻止调用它也将是取消。取消自动向下传播。

In the above example, we're expressing our concurrency requirement using race. If take(LOGOUT) wins the race (i.e. user clicked on a Logout Button). The race will automatically cancel the authAndRefreshTokenOnExpiry background task. And if the authAndRefreshTokenOnExpiry was blocked in middle of a call(authorize, {token}) call it'll also be cancelled. Cancellation propagates downward automatically.

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

这篇关于使用带有ES6发生器的redux-saga与带有ES2017异步/等待的redux-thunk的优点/缺点的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆