与Redux合作的每个人迟早都会遇到异步操作的问题。 但是没有它们,就无法开发现代的应用程序。 这些是对后端的http请求,以及各种计时器/延迟。 Redux的创建者本身就明确地说过-默认情况下,仅支持同步数据流,所有异步操作都必须放在中间件中。
当然,这太冗长和不便,因此很难找到只使用“本机”中间件的开发人员。 诸如Thunk,Saga等之类的库和框架总是可以帮助您。
对于大多数任务,它们就足够了。 但是,如果需要比发送一个请求或发出一个计时器更复杂的逻辑怎么办? 这是一个小例子:
async dispatch => { setTimeout(() => { try { await Promise .all([fetchOne, fetchTwo]) .then(([respOne, respTwo]) => { dispatch({ type: 'SUCCESS', respOne, respTwo }); }); } catch (error) { dispatch({ type: 'FAILED', error }); } }, 2000); }
即使看这样的代码也很痛苦,但是根本无法维护和扩展。 当需要更复杂的错误处理时该怎么办? 如果您需要重复请求怎么办? 如果我想重用此功能?
我的名字叫Dmitry Samokhvalov,在这篇文章中,我将告诉您Observable是什么概念,以及如何与Redux结合使用,并将所有这些与Redux-Saga的功能进行比较。
通常,在这种情况下,请使用redux-saga。 好的,我们重写sagas:
try { yield call(delay, 2000); const [respOne, respTwo] = yield [ call(fetchOne), call(fetchTwo) ]; yield put({ type: 'SUCCESS', respOne, respTwo }); } catch (error) { yield put({ type: 'FAILED', error }); }
它已经变得明显更好-代码几乎是线性的,外观和读取效果更好。 但是,扩展和重用仍然是困难的,因为传奇和重击一样势在必行。
还有另一种方法。 这就是方法,而不仅仅是另一个用于编写异步代码的库。 它称为Rx(它们也是Observable,Reactive Streams等)。 我们将使用它并在Observable上重写示例:
action$ .delay(2000) .switchMap(() => Observable.merge(fetchOne, fetchTwo) .map(([respOne, respTwo]) => ({ type: 'SUCCESS', respOne, respTwo })) .catch(error => ({ type: 'FAILED', error }))
该代码不仅变得平坦而且数量减少,描述异步操作的原理也发生了变化。 现在,我们不直接处理查询,而是对称为Observable的特殊对象执行操作。
将Observable表示为给出值流(序列)的函数很方便。 可观察具有三个主要状态-下一个(“给出下一个值”),错误(“发生错误”)和完成(“值结束,没有其他可提供的”)。 在这方面,它有点像Promise,但是不同之处在于可以迭代这些值(这是Observable超级大国之一)。 您可以将任何内容包装在Observable中-超时,http请求,DOM事件,仅是js对象。

可观察到的第二个超级大国是算子。 运算符是一个接受并返回Observable的函数,但对值流执行某些操作。 最接近的类比是来自javascript的地图和过滤器(顺便说一下,此类运算符在Rx中)。

就我个人而言,最有用的是zip,forkJoin和flatMap运算符。 使用他们的示例,最容易解释操作员的工作。
zip运算符的工作非常简单-它需要几个Observable(不超过9个),并在数组中返回它们发出的值。
const first = fromEvent("mousedown"); const second = fromEvent("mouseup"); zip(first, second) .subscribe(e => console.log(`${e[0].x} ${e[1].x}`));
通常,zip的工作可以由以下方案表示:

如果您有多个Observable,并且需要始终如一地从中接收值,则使用Zip(尽管它们可以以不同的间隔(无论是否同步)发出)。 在处理DOM事件时,它非常有用。
forkJoin语句与zip类似,但有一个例外-它仅返回每个Observable中的最新值。

因此,当仅需要流中的有限值时使用它是合理的。
flatMap运算符稍微复杂一点。 它以一个Observable作为输入,并返回一个新的Observable,并使用选择器函数或另一个Observable将其值映射到新的Observable。 听起来令人困惑,但是该图非常简单:

代码更清晰:
const observable = of("Hello"); const promise = value => new Promise(resolve => resolve(`${value} World`); observable .flatMap(value => promise(value)) .subscribe(result => console.log(result));
通常,flatMap与switchMap和concatMap一起用于后端请求中。
如何在Redux中使用Rx? 有一个很棒的redux-observable库。 其架构如下所示:

所有可观察的运算符及其上的动作均以称为epic的特殊中间件的形式实现。 每个史诗将动作作为输入,将其包装在Observable中,并应将动作也作为Observable返回。 您无法返回常规动作,这将导致无限循环。 让我们写一个小史诗,向api发出请求。
const fetchEpic = action$ => action$ .ofType('FETCH_INFO') .map(() => ({ type: 'FETCH_START' })) .flatMap(() => Observable .from(apiRequest) .map(data => ({ type: 'FETCH_SUCCESS', data })) .catch(error => ({ type: 'FETCH_ERROR', error })) )
不比较redux-observable和redux-saga是不可能的。 在许多人看来,它们在功能和功能上都很接近,但事实并非如此。 Sagas是一个绝对必要的工具,本质上是一组用于处理副作用的方法。 可观察的是,编写异步代码的根本不同的风格(如果需要的话)是不同的哲学。
我写了几个例子来说明解决问题的可能性和方法。
假设我们需要实现一个计时器,该计时器将通过操作停止。 这是萨加斯人的样子:
while(true) { const timer = yield race({ stopped: take('STOP'), tick: call(wait, 1000) }) if (!timer.stopped) { yield put(actions.tick()) } else { break } }
现在使用Rx:
interval(1000) .takeUntil(action$.ofType('STOP'))
假设存在一个任务,以在sagas中实现带取消的请求:
function* fetchSaga() { yield call(fetchUser); } while (yield take('FETCH')) { const fetchSaga = yield fork(fetchSaga); yield take('FETCH_CANCEL'); yield cancel(fetchSaga); }
在Rx上,一切都更加简单:
switchMap(() => fetchUser()) .takeUntil(action$.ofType('FETCH_CANCEL'))
最后,我的最爱。 实施api请求,以防万一失败,重复请求不超过5个,延迟2秒。 这就是我们的传奇故事:
for (let i = 0; i < 5; i++) { try { const apiResponse = yield call(apiRequest); return apiResponse; } catch (err) { if(i < 4) { yield delay(2000); } } } throw new Error(); }
Rx会发生什么:
.retryWhen(errors => errors .delay(1000) .take(5))
如果您总结此传奇的利弊,则会得到以下图片:

Sagas易于学习且非常受欢迎,因此在社区中,您几乎可以在所有场合找到食谱。 不幸的是,命令式样式确实无法灵活使用sagas。
Rx的情况完全不同:

Rx似乎是一把魔术锤和银弹。 不幸的是,事实并非如此。 输入Rx的门槛要高得多,因此很难将一个新人介绍给积极使用Rx的项目。
此外,在使用Observable时,特别要小心并始终很好地了解正在发生的事情,这一点尤其重要。 否则,您可能会发现明显的错误或未定义的行为。
action$ .ofType('DELETE') .switchMap(() => Observable .fromPromise(deleteRequest) .map(() => ({ type: 'DELETE_SUCCESS'})))
一旦我写了一部完成了相当简单的史诗-每种类型为'DELETE'的动作,就会调用一个API方法来删除该项目。 但是,测试期间存在问题。 测试人员抱怨行为异常-有时,当您单击删除按钮时,什么也没有发生。 事实证明,switchMap运算符一次仅支持一个Observable的执行,这是一种防止竞争条件的保护措施。
因此,我将提供一些建议,我会遵循这些建议,并敦促开始使用Rx的每个人都应遵循:
- 小心点
- 检查文档。
- 签入沙箱。
- 编写测试。
- 不要从加农炮射麻雀。