使用带有 ES6 生成器的 redux-saga 与带有 ES2017 async/await 的 redux-thunk 的优缺点

新手上路,请多包涵

现在有很多关于 redux town 的最新小子 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...

原文由 hampusohlsson 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 343
2 个回答

在 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 )。即动作不断地 被推 送给 thunk,而 thunk 无法控制何时停止处理这些动作。

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

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

  • 处理 LOGOUT 用户操作

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

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

你会如何用thunks来实现它?同时还为整个流程提供完整的测试覆盖率?以下是 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}) 调用中被阻塞,它也会被取消。取消会自动向下传播。

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

原文由 Yassine Elouafi 发布,翻译遵循 CC BY-SA 3.0 许可协议

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

专业版(使用传奇):

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

  • redux-saga 带有很多有用的任务辅助函数。在我看来,saga 的概念是为您的应用程序创建某种后台工作程序/线程,它充当 react redux 架构中缺失的部分(actionCreators 和 reducers 必须是纯函数。)这导致了下一点。

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

缺点:

  • 生成器语法。

  • 很多概念要学。

  • API稳定性。似乎 redux-saga 仍在添加功能(例如频道?)并且社区没有那么大。如果图书馆某天进行了非向后兼容的更新,则令人担忧。

原文由 yjcxy12 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题