使用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
后台任务。 如果authAndRefreshTokenOnExpiry
在call(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
) - 取消现有的程序(
cancel
,takeLatest
,race
) - 多个例程可以听同一个动作(
take
,takeEvery
,…)
萨加斯还提供了其他有用的function,它概括了一些常见的应用程序模式:
- 监听外部事件源的
channels
(例如websockets) - 叉子模型(
fork
,spawn
) - 风门
- …
萨加斯是伟大而强大的工具。 然而,这个悍将来自责任。 当你的应用程序增长的时候,你可以很容易地弄清楚谁正在等待被调度的行动,或者是什么事情发生在一些行动被派遣。 另一方面,thunk更简单,更容易推理。 select一个或另一个取决于许多方面,如项目的types和大小,您的项目必须处理什么types的副作用或开发团队偏好。 无论如何,只要保持你的应用程序简单和可预测。
这是一个项目,结合了redux-saga
和redux-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动作逻辑。
- 不需要其他Reduxasynchronous中间件。 例如thunk,promise-middleware,saga
- 轻松地允许您将承诺传递给redux 并让它为您pipe理
- 允许您将外部服务呼叫与他们将要转换的位置共同定位
- 命名文件“init.js”将在应用程序启动时调用一次。 这对于在启动时从服务器加载数据很有用
这个想法是让每个动作都在一个特定的文件中 。 在文件中将服务器调用与“还原”,“已完成”和“已拒绝”的还原器function共同定位。 这使得处理承诺非常容易。
它还会自动将一个辅助对象(称为“asynchronous”)附加到您的状态原型,从而允许您在UI中跟踪所请求的转换。