Javascript 使用带有 ES6 生成器的 redux-saga 与带有 ES2017 async/await 的 redux-thunk 的优缺点
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/34930735/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Pros/cons of using redux-saga with ES6 generators vs redux-thunk with ES2017 async/await
提问by hampusohlsson
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.
现在有很多关于 redux 镇上最新的孩子redux-saga/redux-saga 的讨论。它使用生成器函数来监听/调度动作。
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
而不是下面我使用redux-thunk
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...
采纳答案by Yassine Elouafi
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)
. 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.
首先要注意的是,我们正在使用表单调用 api 函数yield call(func, ...args)
。call
不执行效果,它只是创建一个普通对象,如{type: 'CALL', func, args}
. 执行委托给 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. Mocking data is way simpler than mocking functions.
请注意,我们通过简单地将模拟数据注入next
迭代器的方法来模拟 api 调用结果。模拟数据比模拟函数简单得多。
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 pushedto thunks, and thunks have no control on when to stop handling those actions.
要注意的第二件事是调用yield take(ACTION)
. 动作创建者在每个新动作(例如LOGIN_REQUEST
)上调用 Thunk 。即动作不断地被推送给 thunk,而 thunk 无法控制何时停止处理这些动作。
In redux-saga, generators pullthe 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.
在 redux-saga 中,生成器拉下一个动作。即他们可以控制什么时候听一些动作,什么时候不听。在上面的例子中,流指令被放置在一个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
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 eachexpires_in
millisecondsTake into account that when waiting for the result of api calls (either initial login or refresh) the user may logout in-between.
处理 LOGOUT 用户操作
在第一次成功登录时,服务器返回一个令牌,该令牌在存储在
expires_in
字段中的某个延迟后到期。我们必须每expires_in
毫秒在后台刷新授权考虑到在等待 api 调用的结果(初始登录或刷新)时,用户可能会在中间注销。
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:
你将如何用 thunk 来实现它;同时还为整个流程提供完整的测试覆盖?以下是 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.
在上面的示例中,我们使用race
. 如果take(LOGOUT)
赢得比赛(即用户点击了注销按钮)。比赛会自动取消authAndRefreshTokenOnExpiry
后台任务。如果在通话过程authAndRefreshTokenOnExpiry
中被阻止,call(authorize, {token})
它也会被取消。取消自动向下传播。
You can find a runnable demo of the above flow
回答by yjcxy12
I will add my experience using saga in production system in addition to the library author's rather thorough answer.
除了库作者相当详尽的答案之外,我还将添加我在生产系统中使用 saga 的经验。
Pro (using saga):
亲(使用传奇):
Testability. It's very easy to test sagas as call() returns a pure object. Testing thunks normally requires you to include a mockStore inside your test.
redux-saga comes with lots of useful helper functions about tasks. 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.
Sagas offer independent place to handle all side effects. It is usually easier to modify and manage than thunk actions in my experience.
可测试性。因为 call() 返回一个纯对象,所以测试 sagas 非常容易。测试 thunk 通常要求您在测试中包含一个 mockStore。
redux-saga 提供了许多关于任务的有用的辅助函数。在我看来,saga 的概念是为您的应用程序创建某种后台工作线程/线程,它们充当 react redux 架构中的一个缺失部分(actionCreators 和 reducers 必须是纯函数。)这就引出了下一点。
Sagas 提供独立的地方来处理所有副作用。根据我的经验,修改和管理通常比 thunk 操作更容易。
Con:
骗局:
Generator syntax.
Lots of concepts to learn.
API stability. It seems redux-saga is still adding features (eg Channels?) and the community is not as big. There is a concern if the library makes a non backward compatible update some day.
生成器语法。
很多概念要学习。
API 稳定性。似乎 redux-saga 仍在添加功能(例如 Channels?)并且社区没有那么大。如果库在某一天进行非向后兼容的更新,则令人担忧。
回答by madox2
I'd just like to add some comments from my personal experience (using both sagas and thunk):
我只想从我的个人经验中添加一些评论(同时使用 sagas 和 thunk):
Sagas are great to test:
Sagas 非常适合测试:
- You don't need to mock functions wrapped with effects
- Therefore tests are clean, readable and easy to write
- When using sagas, action creators mostly return plain object literals. It is also easier to test and assert unlike thunk's promises.
- 您不需要模拟用效果包装的函数
- 因此,测试干净、可读且易于编写
- 使用 sagas 时,动作创建者主要返回纯对象文字。与 thunk 的承诺不同,它也更容易测试和断言。
Sagas are more powerful. 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). For example:
传奇更强大。你可以在一个 thunk 的 action creator 中做的所有事情你也可以在一个传奇中做,但反之则不行(或者至少不容易)。例如:
- wait for an action/actions to be dispatched (
take
) - cancel existing routine (
cancel
,takeLatest
,race
) - multiple routines can listen to the same action (
take
,takeEvery
, ...)
- 等待一个或多个动作被分派 (
take
) - 取消现有例程 (
cancel
,takeLatest
,race
) - 多个例程可以侦听相同的动作 (
take
,takeEvery
, ...)
Sagas also offers other useful functionality, which generalize some common application patterns:
Sagas 还提供了其他有用的功能,这些功能概括了一些常见的应用程序模式:
channels
to listen on external event sources (e.g. websockets)- fork model (
fork
,spawn
) - throttle
- ...
channels
侦听外部事件源(例如 websockets)- 前叉模型 (
fork
,spawn
) - 风门
- ...
Sagas are great and powerful tool. 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. 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.
传奇是伟大而强大的工具。然而,权力伴随着责任。当您的应用程序增长时,您可以通过弄清楚谁在等待调度操作,或者在调度某些操作时发生的一切而很容易迷失方向。另一方面,thunk 更简单,更容易推理。选择一个或另一个取决于许多方面,例如项目的类型和规模、您的项目必须处理的副作用类型或开发团队的偏好。在任何情况下,只要让您的应用程序简单且可预测即可。
回答by Jonathan
Just some personal experience:
只是一些个人经验:
For coding style and readability, one of the most significant advantages of using redux-saga in the past is to avoid callback hell in redux-thunk?—?one does not need to use many nesting then/catch anymore. But now with the popularity of async/await thunk, one could also write async code in sync style when using redux-thunk, which may be regarded as an improvement in redux-think.
One may need to write much more boilerplate code when using redux-saga, especially in Typescript. For example, if one wants to implement a fetch async function, the data and error handling could be directly performed in one thunk unit in action.js with one single FETCH action. But in redux-saga, one may need to define FETCH_START, FETCH_SUCCESS and FETCH_FAILURE actions and all their related type-checks, because one of the features in redux-saga is to use this kind of rich “token” mechanism to create effects and instruct redux store for easy testing. Of course one could write a saga without using these actions, but that would make it similar to a thunk.
In terms of file structure, redux-saga seems to be more explicit in many cases. One could easily find an async related code in every sagas.ts, but in redux-thunk, one would need to see it in actions.
Easy testing may be another weighted feature in redux-saga. This is truly convenient. But one thing that needs to be clarified is that redux-saga “call” test would not perform actual API call in testing, thus one would need to specify the sample result for the steps which may use it after the API call. Therefore before writing in redux-saga, it would be better to plan a saga and its corresponding sagas.spec.ts in detail.
Redux-saga also provides many advanced features such as running tasks in parallel, concurrency helpers like takeLatest/takeEvery, fork/spawn, which are far more powerful than thunks.
对于编码风格和可读性,过去使用 redux-saga 的最显着优势之一是避免了 redux-thunk 中的回调地狱?——不再需要使用许多嵌套 then/catch。但是现在随着 async/await thunk 的流行,在使用 redux-thunk 的时候也可以写同步风格的 async 代码,这可以算是 redux-think 的一个改进。
使用 redux-saga 时可能需要编写更多样板代码,尤其是在 Typescript 中。例如,如果想要实现一个 fetch async 功能,数据和错误处理可以直接在 action.js 中的一个 thunk 单元中执行,并使用一个 FETCH 操作。但是在 redux-saga 中,可能需要定义 FETCH_START、FETCH_SUCCESS 和 FETCH_FAILURE 动作及其所有相关的类型检查,因为 redux-saga 的特性之一就是使用这种丰富的“令牌”机制来创建效果和指示redux store 便于测试。当然,人们可以在不使用这些动作的情况下编写 saga,但这会使其类似于 thunk。
在文件结构方面,redux-saga 在很多情况下似乎更加明确。人们可以很容易地在每个 sagas.ts 中找到与异步相关的代码,但在 redux-thunk 中,人们需要在操作中看到它。
轻松测试可能是 redux-saga 中的另一个加权特性。这真的很方便。但是需要澄清的一点是,redux-saga“调用”测试不会在测试中执行实际的API调用,因此需要指定API调用后可能使用的步骤的示例结果。因此,在使用 redux-saga 编写之前,最好详细规划一个 saga 及其对应的 sagas.spec.ts。
Redux-saga 还提供了许多高级功能,例如并行运行任务,并发助手如 takeLatest/takeEvery、fork/spawn,这些功能远比 thunk 强大。
In conclusion, personally, I would like to say: in many normal cases and small to medium size apps, go with async/await style redux-thunk. It would save you many boilerplate codes/actions/typedefs, and you would not need to switch around many different sagas.ts and maintain a specific sagas tree. But if you are developing a large app with much complex async logic and the need for features like concurrency/parallel pattern, or have a high demand for testing and maintenance (especially in test-driven development), redux-sagas would possibly save your life.
总之,我个人想说:在许多正常情况下和中小型应用程序中,使用 async/await 风格的 redux-thunk。它可以为您节省许多样板代码/操作/typedef,并且您不需要切换许多不同的 sagas.ts 并维护特定的 sagas 树。但是,如果您正在开发一个具有非常复杂的异步逻辑并且需要并发/并行模式等功能的大型应用程序,或者对测试和维护有很高的需求(尤其是在测试驱动开发中),redux-sagas 可能会挽救您的生命.
Anyway, redux-saga is not more difficult and complex than redux itself, and it does not have a so-called steep learning curve because it has well-limited core concepts and APIs. Spending a small amount of time learning redux-saga may benefit yourself one day in the future.
无论如何,redux-saga 并不比 redux 本身更困难和复杂,并且它没有所谓的陡峭学习曲线,因为它的核心概念和 API 非常有限。花少量时间学习 redux-saga 可能在未来的某一天让自己受益。
回答by David Bradshaw
Having reviewed a few different large scale React/Redux projects in my experience Sagas provide developers a more structured way of writing code that is much easier to test and harder to get wrong.
根据我的经验,回顾了几个不同的大型 React/Redux 项目后,Sagas 为开发人员提供了一种更结构化的代码编写方式,更容易测试且更难出错。
Yes it is a little wierd to start with, but most devs get enough of an understanding of it in a day. I always tell people to not worry about what yield
does to start with and that once you write a couple of test it will come to you.
是的,一开始有点奇怪,但大多数开发人员在一天之内就对它有足够的了解。我总是告诉人们不要担心从什么yield
开始,一旦你写了几个测试,它就会出现在你身上。
I have seen a couple of projects where thunks have been treated as if they are controllers from the MVC patten and this quickly becomes an unmaintable mess.
我见过几个项目,其中 thunk 被视为来自 MVC 模式的控制器,这很快就变成了一个无法维护的混乱。
My advice is to use Sagas where you need A triggers B type stuff relating to a single event. For anything that could cut across a number of actions, I find it is simpler to write customer middleware and use the meta property of an FSA action to trigger it.
我的建议是在需要 A 触发与单个事件相关的 B 类型内容的地方使用 Sagas。对于可以跨越多个操作的任何事情,我发现编写客户中间件并使用 FSA 操作的元属性来触发它更简单。
回答by Mselmi Ali
Thunks versus Sagas
Thunks 与 Sagas
Redux-Thunk
and Redux-Saga
differ in a few important ways, both are middleware libraries for Redux (Redux middleware is code that intercepts actions coming into the store via the dispatch() method).
Redux-Thunk
并且Redux-Saga
在一些重要方面有所不同,两者都是 Redux 的中间件库(Redux 中间件是拦截通过 dispatch() 方法进入 store 的操作的代码)。
An action can be literally anything, but if you're following best practices, an action is a plain javascript object with a type field, and optional payload, meta, and error fields. e.g.
一个动作实际上可以是任何东西,但如果你遵循最佳实践,一个动作是一个普通的 javascript 对象,带有一个类型字段,以及可选的有效负载、元数据和错误字段。例如
const loginRequest = {
type: 'LOGIN_REQUEST',
payload: {
name: 'admin',
password: '123',
}, };
Redux-Thunk
Redux-Thunk
In addition to dispatching standard actions, Redux-Thunk
middleware allows you to dispatch special functions, called thunks
.
除了分派标准动作之外,Redux-Thunk
中间件还允许您分派称为thunks
.
Thunks (in Redux) generally have the following structure:
Thunk(在 Redux 中)通常具有以下结构:
export const thunkName =
parameters =>
(dispatch, getState) => {
// Your application logic goes here
};
That is, a thunk
is a function that (optionally) takes some parameters and returns another function. The inner function takes a dispatch function
and a getState
function -- both of which will be supplied by the Redux-Thunk
middleware.
也就是说, athunk
是一个函数,它(可选)接受一些参数并返回另一个函数。内部函数采用 adispatch function
和 agetState
函数——两者都将由Redux-Thunk
中间件提供。
Redux-Saga
Redux-Saga
Redux-Saga
middleware allows you to express complex application logic as pure functions called sagas. Pure functions are desirable from a testing standpoint because they are predictable and repeatable, which makes them relatively easy to test.
Redux-Saga
中间件允许您将复杂的应用程序逻辑表达为称为 saga 的纯函数。从测试的角度来看,纯函数是可取的,因为它们是可预测和可重复的,这使得它们相对容易测试。
Sagas are implemented through special functions called generator functions. These are a new feature of ES6 JavaScript
. Basically, execution jumps in and out of a generator everywhere you see a yield statement. Think of a yield
statement as causing the generator to pause and return the yielded value. Later on, the caller can resume the generator at the statement following the yield
.
Sagas 是通过称为生成器函数的特殊函数实现的。这些是ES6 JavaScript
. 基本上,在您看到 yield 语句的任何地方,执行都会进出生成器。将yield
语句视为导致生成器暂停并返回生成的值。稍后,调用者可以在yield
.
A generator function is one defined like this. Notice the asterisk after the function keyword.
生成器函数就是这样定义的。注意 function 关键字后面的星号。
function* mySaga() {
// ...
}
Once the login saga is registered with Redux-Saga
. But then the yield
take on the the first line will pause the saga until an action with type 'LOGIN_REQUEST'
is dispatched to the store. Once that happens, execution will continue.
一旦登录 saga 注册到Redux-Saga
. 但是yield
第一行的take 会暂停 saga,直到一个带有 type 的 action'LOGIN_REQUEST'
被分派到 store。一旦发生这种情况,执行将继续。
回答by Dmitriy
One quick note. Generators are cancellable, async/await — not. So for an example from the question, it does not really make sense of what to pick. But for more complicated flows sometimes there is no better solution than using generators.
一个快速说明。生成器是可取消的,异步/等待 - 不是。因此,对于问题中的一个示例,选择什么并没有真正意义。但是对于更复杂的流程,有时没有比使用生成器更好的解决方案。
So, another idea could be is to use generators with redux-thunk, but for me, it seems like trying to invent a bicycle with square wheels.
所以,另一个想法可能是使用带有 redux-thunk 的生成器,但对我来说,这似乎是在尝试发明一辆方形轮子的自行车。
And of course, generators are easier to test.
当然,生成器更容易测试。
回答by Diego Haz
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/redux-saga-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)
})
}
}
回答by codemeasandwich
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. To accompany your "default" function action logic.
redux-auto 通过允许您创建一个返回承诺的“动作”函数来简单地解决这个异步问题。伴随您的“默认”功能动作逻辑。
- No need for other Redux async middleware. e.g. thunk, promise-middleware, saga
- Easily allows you to pass a promise into redux and have it managed for you
- Allows you to co-locate external service calls with where they will be transformed
- Naming the file "init.js" will call it once at app start. This is good for loading data from the server at start
- 不需要其他 Redux 异步中间件。例如 thunk、promise-middleware、saga
- 轻松允许您将承诺传递给 redux并为您管理
- 允许您将外部服务调用与它们将被转换的位置放在一起
- 将文件命名为“init.js”将在应用程序启动时调用一次。这有利于在开始时从服务器加载数据
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". This makes handling promises very easy.
这个想法是将每个动作放在一个特定的文件中。将文件中的服务器调用与“待处理”、“已完成”和“已拒绝”的reducer 函数并置。这使得处理 promise 变得非常容易。
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 中跟踪请求的转换。