本文翻译自:Pros/cons of using redux-saga with ES6 generators vs redux-thunk with ES2017 async/await
There is a lot of talk about the latest kid in redux town right now, redux-saga/redux-saga . 现在有很多关于redux镇最新孩子的讨论, 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. 在我绕过它之前,我想知道使用redux-saga
而不是下面的方法的优点/缺点,我在使用带有async / await的redux-thunk
。
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 (
);
}
}
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...
参考:https://stackoom.com/question/2MZ4x/使用带有ES-发生器的redux-saga与带有ES-异步-等待的redux-thunk的优点-缺点
In redux-saga, the equivalent of the above example would be 在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 });
}
}
The first thing to notice is that we're calling the api functions using the form yield call(func, ...args)
. 首先要注意的是我们使用表单yield call(func, ...args)
来调用api函数。 call
doesn't execute the effect, it just creates a plain object like {type: 'CALL', func, args}
. call
不执行效果,它只是创建一个像{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-saga中间件,后者负责执行该函数并使用其结果恢复生成器。
The main advantage is that you can test the generator outside of Redux using simple equality checks 主要优点是您可以使用简单的相等性检查在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 })
)
Note we're mocking the api call result by simply injecting the mocked data into the next
method of the iterator. 注意我们只是通过将模拟数据注入迭代器的next
方法来模拟api调用结果。 Mocking data is way simpler than mocking functions. 模拟数据比模拟函数更简单。
The second thing to notice is the call to yield take(ACTION)
. 要注意的第二件事是对yield take(ACTION)
。 Thunks are called by the action creator on each new action (eg LOGIN_REQUEST
). 动作创建者在每个新动作上调用LOGIN_REQUEST
(例如LOGIN_REQUEST
)。 ie actions are continually pushed to thunks, and thunks have no control on when to stop handling those actions. 即动作不断被推到thunk,并且thunk无法控制何时停止处理这些动作。
In redux-saga, generators pull the next action. 在终极版,传奇,发电机拉下一个动作。 ie 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. 在上面的示例中,流指令被放置在while(true)
循环内,因此它将侦听每个传入的操作,这有点模仿thunk推送行为。
The pull approach allows implementing complex control flows. 拉方法允许实现复杂的控制流程。 Suppose for example we want to add the following requirements 例如,假设我们要添加以下要求
Handle LOGOUT user action 处理LOGOUT用户操作
upon the first successful login, the server returns a token which expires in some delay stored in a expires_in
field. 在第一次成功登录时,服务器返回一个令牌,该令牌在存储在expires_in
字段中的一些延迟中到期。 We'll have to refresh the authorization in the background on each expires_in
milliseconds 我们必须在每个expires_in
毫秒的后台刷新授权
Take into account that when waiting for the result of api calls (either initial login or refresh) the user may logout in-between. 考虑到在等待api调用的结果(初始登录或刷新)时,用户可以在中间注销。
How would you implement that with thunks; 你如何用thunk实现它; while also providing full test coverage for the entire flow? 同时还为整个流程提供全面的测试覆盖? Here is how it may look with Sagas: 以下是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
. 在上面的例子中,我们使用race
来表达我们的并发性要求。 If take(LOGOUT)
wins the race (ie user clicked on a Logout Button). 如果take(LOGOUT)
赢得比赛(即用户点击了Logout按钮)。 The race will automatically cancel the authAndRefreshTokenOnExpiry
background task. 比赛将自动取消authAndRefreshTokenOnExpiry
后台任务。 And if the authAndRefreshTokenOnExpiry
was blocked in middle of a call(authorize, {token})
call it'll also be cancelled. 如果authAndRefreshTokenOnExpiry
在call(authorize, {token})
中间被阻止call(authorize, {token})
则它也将被取消。 Cancellation propagates downward automatically. 取消自动向下传播。
You can find a runnable demo of the above flow 您可以找到上述流程的可运行演示
I will add my experience using saga in production system in addition to the library author's rather thorough answer. 除了图书馆作者的相当全面的答案之外,我将在生产系统中添加使用saga的经验。
Pro (using saga): Pro(使用传奇):
Testability. 可测性。 It's very easy to test sagas as call() returns a pure object. 测试sagas非常容易,因为call()返回一个纯对象。 Testing thunks normally requires you to include a mockStore inside your test. 测试thunk通常需要在测试中包含mockStore。
redux-saga comes with lots of useful helper functions about tasks. redux-saga附带了许多有关任务的有用辅助函数。 It seems to me that the concept of saga is to create some kind of background worker/thread for your app, which act as a missing piece in react redux architecture(actionCreators and reducers must be pure functions.) Which leads to next point. 在我看来,saga的概念是为你的app创建某种后台工作者/线程,它在react redux体系结构中扮演一个缺失的部分(actionCreators和reducers必须是纯函数。)这导致了下一点。
Sagas offer independent place to handle all side effects. Sagas提供独立的处理所有副作用的地方。 It is usually easier to modify and manage than thunk actions in my experience. 根据我的经验,修改和管理通常比thunk动作更容易。
Con: 缺点:
Generator syntax. 生成器语法。
Lots of concepts to learn. 要学习很多概念。
API stability. API稳定性。 It seems redux-saga is still adding features (eg Channels?) and the community is not as big. 似乎redux-saga仍在添加功能(例如频道?),社区不是那么大。 There is a concern if the library makes a non backward compatible update some day. 如果库有一天会进行非向后兼容的更新,则会引起关注。
Here's a project that combines the best parts (pros) of both redux-saga
and redux-thunk
: you can handle all side-effects on sagas while getting a promise by dispatching
the corresponding action: https://github.com/diegohaz/redux-saga-thunk 这是一个结合了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)
})
}
}
An easier way is to use redux-auto . 更简单的方法是使用redux-auto 。
from the documantasion 来自文档
redux-auto fixed this asynchronous problem simply by allowing you to create an "action" function that returns a promise. redux-auto简单地通过允许您创建一个返回promise的“action”函数来修复此异步问题。 To accompany your "default" function action logic. 伴随你的“默认”功能动作逻辑。
The idea is to have each action in a specific file . 我们的想法是将每个操作都放在特定的文件中 。 co-locating the server call in the file with reducer functions for "pending", "fulfilled" and "rejected". 将文件中的服务器调用与“pending”,“fulfilled”和“rejected”的reducer函数共同定位。 This makes handling promises very easy. 这使得处理承诺变得非常容易。
It also automatically attaches a helper object(called "async") to the prototype of your state, allowing you to track in your UI, requested transitions. 它还会自动将辅助对象(称为“异步”)附加到您的状态原型,允许您在UI中跟踪请求的转换。
I'd just like to add some comments from my personal experience (using both sagas and thunk): 我只想从我的个人经历中添加一些评论(使用传奇和thunk):
Sagas are great to test: Sagas非常适合测试:
Sagas are more powerful. Sagas更强大。 All what you can do in one thunk's action creator you can also do in one saga, but not vice versa (or at least not easily). 您可以在一个thunk的动作创建者中执行所有操作,您也可以在一个传奇中执行,但反之亦然(或者至少不容易)。 For example: 例如:
take
) 等待行动/行动被派遣( take
) cancel
, takeLatest
, race
) 取消现有的例行程序( cancel
, takeLatest
race
, race
) take
, takeEvery
, ...) 多个例程可以听同样的动作( take
, takeEvery
,...) Sagas also offers other useful functionality, which generalize some common application patterns: Sagas还提供其他有用的功能,它们概括了一些常见的应用程序模式:
channels
to listen on external event sources (eg websockets) channels
侦听的外部事件的来源(例如WebSockets的) fork
, spawn
) fork模型( fork
, spawn
) Sagas are great and powerful tool. Sagas是伟大而强大的工具。 However with the power comes responsibility. 然而,权力来自责任。 When your application grows you can get easily lost by figuring out who is waiting for the action to be dispatched, or what everything happens when some action is being dispatched. 当您的应用程序增长时,您可以通过确定谁正在等待调度操作,或者在调度某些操作时发生的一切情况而轻易丢失。 On the other hand thunk is simpler and easier to reason about. 另一方面,thunk更简单,更容易推理。 Choosing one or another depends on many aspects like type and size of the project, what types of side effect your project must handle or dev team preference. 选择一个或另一个取决于许多方面,如项目的类型和大小,项目必须处理的副作用类型或开发团队偏好。 In any case just keep your application simple and predictable. 在任何情况下,只需保持您的应用程序简单和可预测。