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

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

问题描述

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

在我考虑它之前,我想知道使用 redux-saga 而不是下面使用 redux-thunk 的方法的优缺点代码> 带有异步/等待.

一个组件可能看起来像这样,像往常一样分派动作.

import { login } from 'redux/auth';类 LoginForm 扩展组件 {点击(e){e.preventDefault();const { 用户,通过 } = this.refs;this.props.dispatch(登录(user.value, pass.value));}使成为() {返回 (<div><input type="text" ref="user"/><input type="password" ref="pass"/><button onClick={::this.onClick}>登录</button>

);}}export default connect((state) => ({}))(LoginForm);

然后我的动作看起来像这样:

//auth.js来自'axios'的导入请求;从 './user' 导入 { loadUserData };//定义常量//定义初始状态//导出默认减速器export const login = (user, pass) =>异步(调度)=>{尝试 {调度({ 类型:LOGIN_REQUEST });let { data } = await request.post('/login', { user, pass });等待调度(loadUserData(data.uid));dispatch({ type: LOGIN_SUCCESS, data });} 捕捉(错误){dispatch({ type: LOGIN_ERROR, error });}}//更多操作...

<小时>

//user.js来自'axios'的导入请求;//定义常量//定义初始状态//导出默认减速器export const loadUserData = (uid) =>异步(调度)=>{尝试 {调度({ 类型:USERDATA_REQUEST });let { data } = await request.get(`/users/${uid}`);dispatch({ type: USERDATA_SUCCESS, data });} 捕捉(错误){dispatch({ type: USERDATA_ERROR, error });}}//更多操作...

解决方案

在 redux-saga 中,上面例子的等价物是

导出函数* loginSaga() {而(真){const { user, pass } = yield take(LOGIN_REQUEST)尝试 {让 { 数据 } = 产出调用(request.post, '/login', { user, pass });yield fork(loadUserData, data.uid);yield put({ type: LOGIN_SUCCESS, data });} 捕捉(错误){yield put({ type: LOGIN_ERROR, error });}}}导出函数* loadUserData(uid) {尝试 {产量放置({ 类型:USERDATA_REQUEST });let { data } = yield call(request.get, `/users/${uid}`);yield put({ type: USERDATA_SUCCESS, data });} 捕捉(错误){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))//用一些虚拟动作恢复生成器const mockAction = {user: '...', pass: '...'}assert.deepEqual(iterator.next(mockAction).value,电话(请求.post,'/登录',模拟操作))//模拟错误结果const mockError = '无效的用户/密码'assert.deepEqual(iterator.throw(mockError).value,put({ type: LOGIN_ERROR, error: mockError }))

请注意,我们通过简单地将模拟数据注入迭代器的 next 方法来模拟 api 调用结果.模拟数据比模拟函数简单得多.

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

在 redux-saga 中,生成器下一个动作.即他们可以控制什么时候听一些动作,什么时候不听.在上面的例子中,流程指令被放置在一个 while(true) 循环中,所以它会监听每个传入的动作,这在某种程度上模仿了 thunk 推送行为.

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

  • 处理注销用户操作

  • 在第一次成功登录时,服务器返回一个令牌,该令牌在存储在 expires_in 字段中的某个延迟后到期.我们必须在每个 expires_in 毫秒

  • 在后台刷新授权
  • 考虑到在等待 api 调用的结果(初始登录或刷新)时,用户可能会在中间退出.

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

function* authorize(credentials) {const token = yield call(api.authorize,凭证)收益放置(登录.成功(令牌))返回令牌}功能* authAndRefreshTokenOnExpiry(名称,密码){let token = yield call(authorize, {name, password})而(真){产生调用(延迟,令牌.expires_in)令牌 = 产生调用(授权,{令牌})}}函数* watchAuth() {而(真){尝试 {const {name, password} = yield take(LOGIN_REQUEST)屈服竞赛([采取(注销),调用(authAndRefreshTokenOnExpiry,名称,密码)])//用户退出,next while 迭代将等待//下一个 LOGIN_REQUEST 操作} 捕捉(错误){产量放置(登录.错误(错误))}}}

在上面的例子中,我们使用 race 来表达我们的并发需求.如果 take(LOGOUT) 赢得比赛(即用户点击了注销按钮).比赛会自动取消authAndRefreshTokenOnExpiry后台任务.如果 authAndRefreshTokenOnExpirycall(authorize, {token}) 调用中间被阻塞,它也会被取消.取消自动向下传播.

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

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.

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

解决方案

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

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.

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

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.

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.

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

  • Handle LOGOUT user action

  • 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

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

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

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.

You can find a runnable demo of the above flow

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

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