在ES6生成器中使用redux-saga vs在ES2017 async/await中使用redux-thunk的优缺点

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

在我把我的头围绕它,我想知道的优点/缺点使用redux-saga而不是下面的方法,我使用redux-thunk与async/await。

组件可能是这样的,像往常一样分派动作。

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...
133558 次浏览

在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)的调用。动作创建者在每个新动作(例如LOGIN_REQUEST)上调用坦克。例如,动作是连续的给坦克,坦克无法控制什么时候停止处理这些动作。

在redux-saga中,生成作为下一个动作。也就是说,他们可以控制什么时候听某些动作,什么时候不听。在上面的例子中,流指令被放置在while(true)循环中,因此它将侦听每个传入的动作,这在某种程度上模仿了thunk push行为。

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

  • 处理注销用户操作

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

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

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

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

除了图书馆作者相当彻底的回答之外,我将添加我在生产系统中使用saga的经验。

优点(使用saga):

  • 可测试性。当call()返回一个纯对象时,测试sagas非常容易。测试坦克通常需要在测试中包含mockStore。

  • Redux-saga提供了许多关于任务的有用的帮助函数。在我看来,saga的概念是为你的应用程序创建某种类型的后台工作人员/线程,作为react redux架构中缺失的一部分(actioncreator和reducers必须是纯函数)。这就引出了下一点。

  • saga提供独立的场所来处理所有副作用。在我的经验中,它通常比坦克动作更容易修改和管理。

反对:

  • 发电机的语法。

  • 有很多概念要学。

  • API的稳定性。redux-saga似乎还在添加新功能(比如Channels?),社区也没有那么大。如果库有一天进行了不向后兼容的更新,就会有问题。

这是一个项目,结合了redux-sagaredux-thunk的最好的部分(优点):你可以处理所有的副作用,同时得到dispatching的承诺,相应的行动: # EYZ0 < / p >
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

来自文献传播

Redux-auto通过允许您创建一个返回承诺的“action”函数来修复这个异步问题。以配合您的“默认”函数动作逻辑。

  1. 不需要其他Redux异步中间件。例如,thunk, promise-middleware, saga
  2. 轻松地允许您将承诺传递到redux# EYZ0
  3. 允许您将外部服务调用与它们将被转换的位置放在一起
  4. 将文件命名为“init.js”将在应用启动时调用它一次。这有利于在启动时从服务器加载数据

这个想法是每个操作。在文件中将服务器调用与“pending”、“completed”和“rejected”的reducer函数放在一起。这使得处理承诺非常容易。

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

我只是想从我的个人经验(使用sagas和thunk)中补充一些评论:

传奇故事很适合测试:

  • 您不需要模拟用效果包装的函数
  • 因此,测试是干净的,可读的,易于编写的
  • 在使用saga时,动作创建者通常会返回简单的对象字面量。与thunk的承诺不同,它也更容易测试和断言。

传说更有力量。你在一个坦克动作创造者中可以做到的所有事情,你也可以在一个传奇中做到,但反之亦然(至少不容易)。例如:

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

Sagas还提供了其他有用的功能,它概括了一些常见的应用程序模式:

  • channels监听外部事件源(例如websockets)
  • 叉型(forkspawn)
  • 节气门
  • ...

传奇故事是伟大而有力的工具。然而,伴随着权力而来的是责任。当应用程序增长时,您很容易弄清楚谁在等待要分派的动作,或者当某个动作被分派时发生了什么。另一方面,thunk更简单,更容易推理。选择哪种方法取决于许多方面,如项目的类型和规模,项目必须处理的副作用类型或开发团队的偏好。在任何情况下,都要保持应用程序的简单性和可预测性。

在我回顾了一些不同的大型React/Redux项目的经验后,Sagas为开发人员提供了一种更结构化的代码编写方式,更容易测试,也更不易出错。

是的,刚开始这有点奇怪,但大多数开发者在一天之内就足够理解它了。我总是告诉人们不要担心yield一开始做了什么,一旦你写了几个测试,它就会出现在你面前。

我曾经见过一些项目,在这些项目中,坦克被当作MVC模式的控制器来处理,这很快就变成了一个不可维护的混乱。

我的建议是,当你需要A触发B类型的与单个事件相关的东西时使用saga。对于任何可能跨越许多操作的东西,我发现编写自定义中间件并使用FSA操作的元属性来触发它更简单。

一个小提示。生成器是可取消的,async/await - not。 举个例子,选什么并没有什么意义。 但对于更复杂的流,有时没有比使用生成器更好的解决方案了。< / p >

所以,另一个想法可能是使用带有还原坦克的发电机,但对我来说,这就像试图发明一辆有方轮子的自行车。

当然,生成器更容易测试。

2020年7月更新:

在过去的16个月里,React社区中最引人注目的变化可能是反应钩子

根据我的观察,为了获得与功能组件和钩子更好的兼容性,项目(即使是那些大型项目)倾向于使用:

  1. 钩子+异步坦克 (hook使一切都非常灵活,所以你实际上可以在你想要的地方放置async thunk,并将其作为普通函数使用,例如,仍然在操作中编写thunk。然后使用dispatch()来触发坦克:https://stackoverflow.com/a/59991104/5256695),
  2. # EYZ0,
  3. GraphQL/Apollo useQuery useMutation
  4. react-fetch -library
  5. 其他流行的数据获取/API调用库、工具、设计模式等

相比之下,与上述方法相比,redux-saga在大多数正常的API调用情况下并没有真正提供显著的好处,同时通过引入许多saga文件/生成器增加了项目的复杂性(也是因为redux-saga的最后一个版本v1.1.1是在2019年9月18日,这是很久以前的事了)。

但是,redux-saga仍然提供了一些独特的特性,例如竞赛效果和并行请求。因此,如果您需要这些特殊的功能,redux-saga仍然是一个不错的选择。


2019年3月原帖:

这是一些个人经历:

  1. 对于编码风格和可读性来说,过去使用redux-saga最重要的优点之一是避免了redux-thunk中的回调地狱——人们不再需要使用很多嵌套then/catch。但现在随着async/await thunk的流行,人们也可以在使用redux-thunk时以同步风格编写异步代码,这可能被视为redux-thunk的改进。

  2. 当使用redux-saga时,可能需要编写更多的样板代码,特别是在Typescript中。例如,如果有人想实现一个fetch异步函数,数据和错误处理可以直接在action.js中的一个thunk单元中使用一个fetch操作执行。但是在redux-saga中,人们可能需要定义FETCH_START、FETCH_SUCCESS和FETCH_FAILURE操作以及所有与它们相关的类型检查,因为redux-saga的特性之一是使用这种丰富的“令牌”机制来创建效果并指示redux存储以方便测试。当然,你也可以不使用这些行动去编写一个传奇故事,但这会让它变得像一个坦克。

  3. 在文件结构方面,redux-saga在许多情况下似乎更明确。人们可以很容易地在每一个传说中找到异步相关的代码。Ts,但在还原坦克中,人们需要看到它的行动。

  4. 简单的测试可能是redux-saga的另一个重要特征。这真是太方便了。但有一点需要澄清的是,redux-saga“调用”测试不会在测试中执行实际的API调用,因此需要为API调用后可能使用的步骤指定示例结果。因此,在编写redux-saga之前,最好详细计划一个saga及其对应的saga .spec.ts。

  5. Redux-saga还提供了许多高级功能,如并行运行任务,并发助手,如takeLatest/takeEvery, fork/spawn,这些功能比坦克强大得多。

总之,就我个人而言,我想说:在许多正常情况下,在中小型应用程序中,使用异步/等待风格的还原操作。这将为您节省许多样板代码/动作/类型,并且您不需要在许多不同的传奇之间切换。Ts和维护一个特定的传奇树。但是如果你正在开发一个具有复杂异步逻辑的大型应用程序,并且需要像并发/并行模式这样的特性,或者对测试和维护有很高的要求(特别是在测试驱动开发中),redux-saga可能会挽救你的生命。

无论如何,redux-saga并不比redux本身更困难和复杂,而且它没有所谓的陡峭的学习曲线,因为它的核心概念和api非常有限。花少量的时间学习redux-saga可能在未来的某一天对你有益。

坦克大战萨加斯

Redux-ThunkRedux-Saga在一些重要的方面有所不同,它们都是Redux的中间件库(Redux中间件是通过dispatch()方法拦截进入存储的操作的代码)。

一个动作可以是任何东西,但是如果您遵循最佳实践,那么一个动作就是一个简单的javascript对象,其中包含一个类型字段,以及可选的有效负载、元和错误字段。如。

const loginRequest = {
type: 'LOGIN_REQUEST',
payload: {
name: 'admin',
password: '123',
}, };

Redux-Thunk

除了分派标准动作之外,Redux-Thunk中间件还允许分派称为thunks的特殊函数。

坦克(在Redux中)通常有以下结构:

export const thunkName =
parameters =>
(dispatch, getState) => {
// Your application logic goes here
};

也就是说,thunk是一个(可选地)接受一些参数并返回另一个函数的函数。内部函数接受dispatch functiongetState函数——这两个函数都将由Redux-Thunk中间件提供。

Redux-Saga

Redux-Saga中间件允许您将复杂的应用程序逻辑表示为称为saga的纯函数。从测试的角度来看,纯函数是可取的,因为它们是可预测和可重复的,这使得它们相对容易测试。

saga是通过称为生成器函数的特殊函数实现的。这是ES6 JavaScript的一个新特性。基本上,在任何看到yield语句的地方,执行都会在生成器中跳跃。可以将yield语句视为导致生成器暂停并返回产生的值。稍后,调用者可以在yield后面的语句处恢复生成器。

生成器函数是这样定义的。注意function关键字后面的星号。

function* mySaga() {
// ...
}

一旦使用Redux-Saga注册了登录传奇。但是第一行的yield将暂停故事,直到一个类型为'LOGIN_REQUEST'的行动被发送到商店。一旦发生这种情况,执行将继续进行。

# EYZ0。

我最近加入了一个大量使用redux-saga的项目,因此也有兴趣了解更多关于saga方法的好处。

说实话,我还在找。读了这篇文章和许多喜欢它的人,“优点”是难以捉摸的。以上的答案似乎可以总结为:

  1. 可测试性(忽略实际的API调用),
  2. 很多辅助函数,
  3. 熟悉服务器端编码的开发人员。

许多其他的说法似乎过于乐观,误导或根本是错误的!我见过许多不合理的说法,“坦克不能做x”。为例。但是坦克是功能。如果一个函数不能做X,那么javascript也不能做X,所以saga也不能做X。

对我来说,缺点是:

  • 使用generator函数混淆了关注点。JS中的生成器返回自定义迭代器。仅此而已。它们没有任何处理异步调用或可取消的特殊能力。任何循环都可以有断点条件,任何函数都可以处理异步请求,任何代码都可以使用自定义迭代器。当人们说:generators have control when to listen for some actiongenerators are cancellable, but async calls are not这样的话,就会造成混淆,因为它暗示这些品质是生成器函数固有的,甚至是唯一的。
  • 不明确的用例:AFAIK SAGA模式用于处理跨服务的并发事务问题。鉴于浏览器是单线程的,很难看出并发性是如何呈现出Promise方法无法处理的问题的。顺便说一句:也很难理解为什么这类问题应该在浏览器中处理。
  • 代码可追溯性:通过使用redux中间件将dispatch转换为一种事件处理,Sagas调度的动作永远不会到达reducers,因此永远不会被redux工具记录。虽然其他库也可以这样做,但考虑到浏览器内置了事件处理,这通常是不必要的复杂。间接的优势也是难以捉摸的,而直接调用传奇将更加明显。

如果这篇文章让我对传奇故事感到沮丧,那是因为我对传奇故事感到沮丧。它们似乎是一个寻找问题的伟大解决方案。国际海事组织。