链式 redux-observable 史诗只正确触发一次 [英] Chained redux-observable epic only fires correctly once

查看:67
本文介绍了链式 redux-observable 史诗只正确触发一次的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经设置了一个等待另一个史诗完成的史诗,就像@jayphelps 在这里的回答:从其他史诗中调用史诗

I've set up an epic that waits for another epic to complete, much like @jayphelps' answer here: Invoking epics from within other epics

但是我发现它似乎只运行一次.之后,我可以在控制台中看到 CART_CONFIG_READY 操作,但未触发 DO_THE_NEXT_THING 操作.

However I've found that it only seems to run once. After that I can see the CART_CONFIG_READY action in the console but the DO_THE_NEXT_THING action is not triggered.

我尝试了 mergeMapswitchMap 的各种组合,有和没有 take 但似乎没有任何帮助.

I've tried various combinations of mergeMap and switchMap, with and without take but nothing seems to help.

这是(某种)我的代码的样子.

This is (kind of) what my code looks like.

import { NgRedux } from '@angular-redux/store';
import { Observable } from 'rxjs/Observable';
import { ActionsObservable } from 'redux-observable';

export class CartEpicsService {

checkCart = (action$: ActionsObservable<any>, store: NgRedux<any>) => {

    return action$.ofType('CHECK_CART')
        .switchMap(() => {

            console.log('___LISTENING___');

            return action$.ofType('CART_CONFIG_READY')
                .take(1) // removing this doesn't help
                .mergeMap(() => {

                    console.log('___RECEIVED___');

                    // do stuff here

                    return Observable.of({
                        type: 'DO_THE_NEXT_THING'
                    });

                })
                .startWith({
                    type: 'GET_CART_CONFIG'
                });

        });

}

getCartConfig = (action$: ActionsObservable<any>, store: NgRedux<any>) => {

    return action$.ofType('GET_CART_CONFIG')
        .switchMap(() => {

            const config = store.getState().config;

            // we already have the config
            if (config) {
                return Observable.of({
                    type: 'CART_CONFIG_READY'
                });
            }

            // otherwise load it from the server using out HTTP service
            return this.http.get('/cart/config')
                .switchMap((response) => {
                    return Observable.concat(
                        Observable.of({
                            type: 'CART_CONFIG_SUCCESS'
                        }),
                        Observable.of({
                            type: 'CART_CONFIG_READY'
                        })
                    );
                })
                .catch(error => Observable.of({
                    type: 'CART_CONFIG_ERROR',
                    error
                }));


        });

    }

}

对于上下文,我需要来自/cart/config 端点的响应来检查购物车的有效性.我只需要下载一次配置.

For context I need the response from the /cart/config endpoint to check the validity of the cart. I only need to download the config once.

这是一个关于 JS Bin 的可运行示例:

Here is a runnable example on JS Bin:

https://jsbin.com/vovejibuwi/1/edit?js,console

推荐答案

Dang 这绝对是一个棘手的问题!

Dang this is definitely a tricky one!

state.config === true 返回一个同步发出的 CART_CONFIG_READY 的 Observable 时,而在第一次 http 请求(或延迟,在 jsbin) 意味着它总是异步的.

When state.config === true you return an Observable of CART_CONFIG_READY that emits synchronously, whereas during the first time the http request (or delay, in the jsbin) means it is always going to be async.

为什么这会有所不同是在 checkCart 史诗中,您返回一个可观察的链,该链通过 action$.ofType('CART_CONFIG_READY')<监听 CART_CONFIG_READY 但也应用了 .startWith({ type: 'GET_CART_CONFIG' }).这意味着 GET_CART_CONFIG 将被同步发出 before action$.ofType('CART_CONFIG_READY') 被订阅,因为 startWith基本上是 concat 的简写,如果您熟悉它,这可能会使问题更加清楚.这与这样做几乎完全一样:

Why this makes a difference is in the checkCart epic you return an observable chain that listens for CART_CONFIG_READY with action$.ofType('CART_CONFIG_READY') but also applies a .startWith({ type: 'GET_CART_CONFIG' }). That means that GET_CART_CONFIG is going to be emitted synconously before action$.ofType('CART_CONFIG_READY') is subscribed because startWith is basically shorthand for a concat, which might would make the issue more clear if you're familiar with it. It's nearly exactly the same as doing this:

Observable.concat(
  Observable.of({
    type: 'GET_CART_CONFIG'
  }),
  action$.ofType('CART_CONFIG_READY') // not subscribed until prior complete()s
    .take(1)
    .mergeMap(() => {
      // stuff
    })
);

总而言之,GET_CART_CONFIG 第二次发生的事情是同步调度的,getCartConfig 接收它并看到配置已经在商店中,所以它同步调度 <代码>CART_CONFIG_READY.但是我们还没有在 checkCart 中监听它,所以它没有得到答复.然后调用栈返回,concat 中的下一个 Observable,即我们的 action$.ofType('CART_CONFIG_READY') 链,被订阅.但是来不及了,它监听的action已经发出了!

So to summarize, what is happening the second time around GET_CART_CONFIG is dispatched synchronously, getCartConfig receives it and sees the config is already in the store so it synchronously dispatches CART_CONFIG_READY. But we are not yet listening for it in checkCart so it goes unanswered. Then that callstack returns and the next Observable in the concat, our action$.ofType('CART_CONFIG_READY') chain, gets subscribed to. But too late, the action it listens for has already been emitted!

解决这个问题的一种方法是让 CART_CONFIG_READY 的发出总是异步的,或者在我们调度 GET_CART_CONFIG 之前在另一个史诗中开始监听它.

One way to fix this is to make either the emitting of CART_CONFIG_READY always async, or to start listening for it in the other epic before we dispatch GET_CART_CONFIG.

Observable.of 接受调度程序作为其最后一个参数,并且 RxJS 支持其中几个.

Observable.of accepts a scheduler as its last argument, and RxJS supports several of them.

在这种情况下,您可以使用 AsyncScheduler(宏任务)或 AsapScheduler(微任务).在这种情况下,两者都可以工作,但它们在 JavaScript 事件循环中安排在不同的时间.如果您不熟悉事件循环任务,请检查一下.

In this case you could use the AsyncScheduler (macrotask) or the AsapScheduler (microtask). Both will work in this case, but they schedule on different times in the JavaScript event loop. If you're not familiar with event loop tasks, check this out.

在这种情况下,我个人建议使用 AsyncSheduler,因为它将提供与发出 http 请求最接近的异步行为.

I would personally recommend using the AsyncSheduler in this case because it will provide the closest async behavior to making an http request.

import { async } from 'rxjs/scheduler/async';

// later inside your epic...

return Observable.of({
  type: 'CART_CONFIG_READY'
}, async);

2.在发出 GET_CART_CONFIG

之前监听 CART_CONFIG_READY

因为 startWithconcat(我们不想这样做)的简写,所以我们需要使用某种形式的 merge,首先使用我们的 ofType 链,以便我们在发出之前监听.

2. Listen for CART_CONFIG_READY before emitting GET_CART_CONFIG

Because startWith is shorthand for a concat (which we don't want to do) we instead need to use some form of merge, with our ofType chain first so that we listen before emitting.

action$.ofType('CART_CONFIG_READY')
  .take(1)
  .mergeMap(() => {
    // stuff
  })
  .merge(
    Observable.of({ type: 'GET_CART_CONFIG' })
  )

// or

Observable.merge(
  action$.ofType('CART_CONFIG_READY')
    .take(1)
    .mergeMap(() => {
      // stuff
    }),
  Observable.of({ type: 'GET_CART_CONFIG' })
)

// both are exactly the same, pick personal preference on appearance

您只需要执行这些解决方案中的一个,但同时执行这两个解决方案也无妨.我可能会建议同时使用两者,以便使事情保持一致且符合预期,即使它们有点冗长.

You only need to do one of these solutions, but it wouldn't hurt to do both of them. Offhand I would probably recommend using both just so that things are consistent and expected, even if they are a bit more verbose.

您可能也很高兴知道 Observable.of 接受任意数量的项目,这些项目将按顺序发出.所以你不需要使用 concat:

You might also be happy to know that Observable.of accepts any number of items, which will be emitted in order. So you don't need to use concat:

// before

Observable.concat(
  Observable.of({
    type: 'CART_CONFIG_SUCCESS'
  }),
  Observable.of({
    type: 'CART_CONFIG_READY'
  })
)

// after

Observable.of({
  type: 'CART_CONFIG_SUCCESS'
}, {
  type: 'CART_CONFIG_READY'
})

非常感谢 jsbin 顺便说一句,它使调试更容易.

Thanks so much for the jsbin btw, it made it much easier to debug.

根据您的评论进行

出于好奇,您是通过经验还是调试发现的?

Out of curiosity did you figure this out through experience or debugging?

两者的结合.我已经处理了大量的异步/调度代码,排序通常是问题的根源.我扫描了代码,在脑海中描绘了执行过程,注意到异步与同步的区别取决于代码路径,然后我做了一个快速操作符,让我可以轻松确认任何 Observable 链的订阅顺序.

A combination of both. I've dealt with a ton of async/scheduled code and ordering is very commonly the source of issues. I scanned the code, mentally picturing execution, noticed the difference in async vs sync depending on codepath, then I made a quick operator to make it easy for me to confirm the order in which any Observable chain is subscribed to.

Observable.prototype.logOnSubscribe = function (msg) {
  // defer is a pretty useful Observable to learn if you haven't yet
  return Observable.defer(() => {
    console.log(msg);
    return this; // the original source
  });
};

我把它应用到了几个地方,但最重要的是这两个:

I applied it to several places, but the most important are these two:

action$.ofType('CART_CONFIG_READY')
  .take(1)
  .mergeMap(() => {
    // stuff
  })
  .logOnSubscribe('listening for CART_CONFIG_READY') // <--- here
  .startWith({
    type: 'GET_CART_CONFIG'
  });

  //  and in the other epic...

  if (hasConfig) {
    return Observable.of({
      type: 'CART_CONFIG_READY'
    })
    .logOnSubscribe('emitting CART_CONFIG_READY');  // <--- and here
  }

它确认在第二个代码路径中 CART_CONFIG_READY 在其他史诗正在侦听它之前被发出.

It confirmed that in the second code path CART_CONFIG_READY was getting emitted before the other epic was listening for it.

这篇关于链式 redux-observable 史诗只正确触发一次的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆