使用ES6发电机的Reduce-saga与使用ES2017asynchronous/等待的redux-thunk的优点/缺点

现在有很多人谈论最新的孩子, yelouafi / redux-saga 。 它使用生成器函数来监听/分派操作。

在我把头围绕之前,我想知道使用redux-saga的优点/缺点,而不是下面的方法,我使用asynchronous/ await使用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... 

在最简单的情况下,上面的例子就相当于

 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不会执行效果,它只是创build一个普通的对象,如{type: 'CALL', func, args} 。 执行被委托给执行该函数并重新生成结果的redux-saga中间件。

主要优点是您可以使用简单的相等性检查来testingRedux之外的生成器

 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调用结果。 嘲笑数据比嘲笑function更简单。

第二件要注意的是调用yield take(ACTION) 。 行动创build者在每个新动作(例如LOGIN_REQUEST )上调用LOGIN_REQUEST 。 即行为不断地被推到黑暗的地方,黑暗无法控制什么时候停止处理这些行动。

发电机组中,发电机组采取下一步行动。 即他们有控制何时听某些行动,什么时候不行。 在上面的例子中,stream程指令被放置在while(true)循环中,所以它会监听每个传入的动作,这有点模仿了thunk推送行为。

拉方法允许实施复杂的控制stream程。 假设我们想要添加以下要求

  • 处理注销用户操作

  • 在第一次成功login时,服务器返回一个存储在expires_in字段中的延迟过期的令牌。 我们将不得不在每个expires_in毫秒内刷新后台的授权

  • 考虑到在等待api调用的结果(初始login或刷新)时,用户可以在两者之间注销。

你怎么用thunk来实现呢? 同时还为整个stream程提供全面的testing覆盖? 以下是萨加斯的外观:

 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来expression我们的并发需求。 如果take(LOGOUT)退出take(LOGOUT)赢得比赛(即用户点击注销button)。 比赛将自动取消authAndRefreshTokenOnExpiry后台任务。 如果authAndRefreshTokenOnExpirycall(authorize, {token})调用中被阻塞,它也将被取消。 取消自动向下传播。

你可以find上面stream程的一个可运行的演示

除了图书馆作者比较透彻的回答外,我还将在生产系统中join我的经验。

临(使用传奇):

  • 可测性。 当call()返回一个纯粹的对象时,testingsagas是非常容易的。 testingthunk通常需要您在testing中包含一个mockStore。

  • redux-saga带有许多有用的帮助函数关于任务。 在我看来,传奇的概念是为你的应用程序创build某种背景工作者/线程,这是作为反应减less架构中的一个缺失部分(actionCreators和reducer必须是纯函数)。这导致下一点。

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

缺点:

  • 生成器语法。

  • 很多的概念要学习。

  • API稳定性。 看起来还有传奇还在增加function(如频道?),而社区并不大。 如果图书馆有一天会进行非向后兼容的更新,那么有一个问题。

我只是想从我的个人经历中join一些评论(同时使用传说和深度):

萨加斯是伟大的testing:

  • 你不需要模拟包含效果的函数
  • 因此,testing是干净的,可读的,易于编写
  • 使用传说时,动作创作者通常会返回普通的对象文字。 不像thunk的承诺,testing和断言也更容易。

萨加斯更加顽强。 所有你能在一个thunk的动作创造者中做的事情,你也可以在一个事件中做,但反之亦然(或者至less不容易)。 例如:

  • 等待一个行动/行动被派遣( take
  • 取消现有的程序( canceltakeLatestrace
  • 多个例程可以听同一个动作( taketakeEvery ,…)

萨加斯还提供了其他有用的function,它概括了一些常见的应用程序模式:

  • 监听外部事件源的channels (例如websockets)
  • 叉子模型( forkspawn
  • 风门

萨加斯是伟大而强大的工具。 然而,这个悍将来自责任。 当你的应用程序增长的时候,你可以很容易地弄清楚谁正在等待被调度的行动,或者是什么事情发生在一些行动被派遣。 另一方面,thunk更简单,更容易推理。 select一个或另一个取决于许多方面,如项目的types和大小,您的项目必须处理什么types的副作用或开发团队偏好。 无论如何,只要保持你的应用程序简单和可预测。

这是一个项目,结合了redux-sagaredux-thunk的最好的部分(优点):你可以处理传说中的所有副作用,同时通过dispatching相应的动作获得承诺: https : //github.com/diegohaz/终极版-佐贺-的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) }) } } 

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

从documantasion

redux-auto简单地通过允许你创build一个返回promise的“action”函数来解决这个asynchronous的问题。 陪伴你的“默认”function动作逻辑。

  1. 不需要其他Reduxasynchronous中间件。 例如thunk,promise-middleware,saga
  2. 轻松地允许您将承诺传递给redux 并让它为您pipe理
  3. 允许您将外部服务呼叫与他们将要转换的位置共同定位
  4. 命名文件“init.js”将在应用程序启动时调用一次。 这对于在启动时从服务器加载数据很有用

这个想法是让每个动作都在一个特定的文件中 。 在文件中将服务器调用与“还原”,“已完成”和“已拒绝”的还原器function共同定位。 这使得处理承诺非常容易。

它还会自动将一个辅助对象(称为“asynchronous”)附加到您的状态原型,从而允许您在UI中跟踪所请求的转换。