RxJs flatMapLatest / switchMap取消回调。 onCancel()在哪里?

时间:2016-08-30 20:16:00

标签: javascript reactive-programming rxjs observable

我有2个嵌套的Observable Streams来执行HTTP请求。现在我想显示一个加载指示器,但无法使其正常工作。

var pageStream = Rx.createObservableFunction(_self, 'nextPage')
        .startWith(1)
        .do(function(pageNumber) {
            pendingRequests++;
        })
        .concatMap(function(pageNumber) {
            return MyHTTPService.getPage(pageNumber);
        })
        .do(function(response) {
            pendingRequests--;
        });

Rx.createObservableFunction(_self, 'search')
        .flatMapLatest(function(e) {
            return pageStream;
        })
        .subscribe();



search();
nextPage(2);
nextPage(3);
search();

这将触发pendingRequests++ 4次,但pendingRequests--仅触发一次,因为flatMapLatest将在前3个HTTP响应到达之前取消内部可观察量。

我找不到类似onCancel回调的内容。我也尝试了onCompletedonError,但这些也不会被flatMapLatest触发。

还有其他方法让这个工作吗?

谢谢!

编辑:所需的加载指示符行为

  1. 示例:单search()次来电。

    • search() - >开始加载指标
    • 当search()响应回来时 - >禁用加载指示器
  2. 示例:search()nextPage()来电。 (nextPage()在 search()回复之前被称为。)

    • search() - >开始加载指标
    • nextPage() - >指标已经启动,但无处可做
    • 两个响应到达后停止加载指标
  3. 示例:search()search()(search()调用相互覆盖,但第一个的响应可以被解除)

    • search() - >开始加载指标
    • search() - >指标已经启动,但无处可做
    • 第二次搜索()的响应到达
    • 时停止加载指示符
  4. 示例:search()nextPage()search()(同样:由于第二次搜索(),可以忽略前一次搜索()和nextPage()的响应。

    • search() - >开始加载指标
    • nextPage() - >指标已经启动,但无处可做
    • search() - >指标已经启动,但无处可做
    • 第二次搜索()的响应到达
    • 时停止加载指示符
  5. 示例:search()nextPage()。但是这次在search()响应回来之后调用nextPage()。

    • search() - >开始加载指标
    • 停止加载指示符,因为search()响应已到达
    • nextPage() - >开始加载指标
    • 停止加载指示符,因为nextPage()响应已到达
  6. 我尝试使用pendingRequests计数器,因为我可以同时拥有多个相关请求(例如:search(), nextPage(), nextPage())。当然,我想在所有相关请求完成后禁用加载指示

    调用search(), search()时,第一个搜索()无关紧要。同样适用于search(), nextPage(), search()。在这两种情况下,只有一个活跃的相关请求(最后search())。

4 个答案:

答案 0 :(得分:2)

一种方法:使用finally运算符(rxjs4 docsrxjs5 source)。无论何时取消订阅或因任何原因完成,都会触发。

我还将计数器逻辑移到concatMap函数内部,因为你实际上是在计算getPage请求,不是经历过的值的数量。这是一个微妙的差异。

var pageStream = Rx.createObservableFunction(_self, 'nextPage')
        .startWith(1)
        .concatMap(function(pageNumber) {
            ++pendingRequests;
            // assumes getPage returns an Observable and not a Promise
            return MyHTTPService.getPage(pageNumber)
               .finally(function () { --pendingRequests; })
        });

答案 1 :(得分:2)

使用public static PrintWriter out又名switchMap,您希望在新外部项到达时尽快修剪当前内部流的执行。这肯定是一个很好的设计决定,否则会带来很多混乱,并允许一些怪异的行动。如果您真的想做某事flatMapLatest,您始终可以使用自定义onCancel回调创建自己的观察对象。但我仍然建议不要将unsubscribe与外部环境的变化状态联系起来。理想情况下,unsubscribe只会清理内部使用的资源。

尽管如此,您可以在不访问unsubscribe或类似内容的情况下解决您的特定情况。关键的观察是 - 如果我正确理解了您的用例 - 在onCancel上可以忽略所有先前/未决的操作。因此,不用担心递减计数器,我们可以简单地从1开始计数。

关于该片段的一些评论:

  • 使用search来计算待处理的请求 - 因为它已准备好与其他流组合;
  • 检查了您在问题中发布的所有案例并且有效;
  • 添加了一些模糊测试来证明正确性;
  • BehaviorSubject仍有待审核时不确定是否允许nextPage - 但似乎只是使用search vs concatMapTo;
  • 仅使用标准merge运算符。

PLNKR

Rx
console.clear();

const searchSub = new Rx.Subject(); // trigger search 
const nextPageSub = new Rx.Subject(); // triger nextPage
const pendingSub = new Rx.BehaviorSubject(); // counts number of pending requests

const randDurationFactory = min => max => () => Math.random() * (max - min) + min;
const randDuration = randDurationFactory(250)(750);
const addToPending = n => () => pendingSub.next(pendingSub.value + n);
const inc = addToPending(1);
const dec = addToPending(-1);

const fakeSearch = (x) => Rx.Observable.of(x)
  .do(() => console.log(`SEARCH-START: ${x}`))
  .flatMap(() => 
    Rx.Observable.timer(randDuration())
    .do(() => console.log(`SEARCH-SUCCESS: ${x}`)))

const fakeNextPage = (x) => Rx.Observable.of(x)
  .do(() => console.log(`NEXT-PAGE-START: ${x}`))
  .flatMap(() =>
    Rx.Observable.timer(randDuration())
    .do(() => console.log(`NEXT-PAGE-SUCCESS: ${x}`)))

// subscribes
searchSub
  .do(() => console.warn('NEW_SEARCH'))
  .do(() => pendingSub.next(1)) // new search -- ingore current state
  .switchMap(
    (x) => fakeSearch(x)
    .do(dec) // search ended
    .concatMapTo(nextPageSub // if you wanted to block nextPage when search still pending
      // .merge(nextPageSub // if you wanted to allow nextPage when search still pending
      .do(inc) // nexpage started
      .flatMap(fakeNextPage) // optionally switchMap
      .do(dec) // nextpage ended
    )
  ).subscribe();

pendingSub
  .filter(x => x !== undefined) // behavior-value initially not defined
  .subscribe(n => console.log('PENDING-REQUESTS', n))

// TEST
const test = () => {
    searchSub.next('s1');
    nextPageSub.next('p1');
    nextPageSub.next('p2');

    setTimeout(() => searchSub.next('s2'), 200)
  }
// test();

// FUZZY-TEST
const COUNTER_MAX = 50;
const randInterval = randDurationFactory(10)(350);
let counter = 0;
const fuzzyTest = () => {
  if (counter % 10 === 0) {
    searchSub.next('s' + counter++)
  }
  nextPageSub.next('p' + counter++);
  if (counter < COUNTER_MAX) setTimeout(fuzzyTest, randInterval());
}

fuzzyTest()

答案 2 :(得分:2)

我从头开始为你的问题写了一个解决方案 当然,它可能以更实用的方式编写,但无论如何它都可以工作。

此解决方案基于reqStack,其中包含请求是具有iddonetype属性的对象的所有请求(保留调用顺序)。< / p>

请求完成后,调用requestEnd方法。 有两个条件,其中至少有一个足以隐藏装载机。

  1. 当堆栈上的最后一个请求是search请求时,我们可以隐藏一个加载器。
  2. 否则,所有其他请求必须已经完成。

    function getInstance() {
     return {
        loaderVisible: false,
        reqStack: [],
    
        requestStart: function (req){
            console.log('%s%s req start', req.type, req.id)
            if(_.filter(this.reqStack, r => r.done == false).length > 0 && !this.loaderVisible){
                this.loaderVisible = true
                console.log('loader visible')
            }
        },
    
        requestEnd: function (req, body, delay){
            console.log('%s%s req end (took %sms), body: %s', req.type, req.id, delay, body)
            if(req === this.reqStack[this.reqStack.length-1] && req.type == 'search'){
                this.hideLoader(req)
                return true
            } else if(_.filter(this.reqStack, r => r.done == true).length == this.reqStack.length && this.loaderVisible){
                this.hideLoader(req)
                return true
            } 
            return false
        },
    
        hideLoader: function(req){
            this.loaderVisible = false
            console.log('loader hidden (after %s%s request)', req.type, req.id)
        },
    
        getPage: function (req, delay) {
            this.requestStart(req)
            return Rx.Observable
                    .fromPromise(Promise.resolve("<body>" + Math.random() + "</body>"))
                    .delay(delay)
        },
    
        search: function (id, delay){
            var req = {id: id, done: false, type: 'search'}
            this.reqStack.push(req)
            return this.getPage(req, delay).map(body => {  
                        _.find(this.reqStack, r => r.id == id && r.type == 'search').done = true
                        return this.requestEnd(req, body, delay)
                    })
        },
    
        nextPage: function (id, delay){
            var req = {id: id, done: false, type: 'nextPage'}
            this.reqStack.push(req)
            return this.getPage(req, delay).map(body => {  
                        _.find(this.reqStack, r => r.id == id && r.type == 'nextPage').done = true
                        return this.requestEnd(req, body, delay)
                    })
        },
    }
    }
    
  3. Moca的单元测试:

    describe('animation loader test:', function() {
    
        var sut
    
        beforeEach(function() {
            sut = getInstance()
        })
    
        it('search', function (done) {
            sut.search('1', 10).subscribe(expectDidHideLoader)
            testDone(done)
        })
    
        it('search, nextPage', function (done) {
            sut.search('1', 50).subscribe(expectDidHideLoader)
            sut.nextPage('1', 20).subscribe(expectDidNOTHideLoader)
            testDone(done)
        })
    
        it('search, nextPage, nextPage', function(done) {
            sut.search('1', 50).subscribe(expectDidHideLoader)
            sut.nextPage('1', 40).subscribe(expectDidNOTHideLoader)
            sut.nextPage('2', 30).subscribe(expectDidNOTHideLoader)
            testDone(done)
        })
    
        it('search, nextPage, nextPage - reverse', function(done) {
            sut.search('1', 30).subscribe(expectDidNOTHideLoader)
            sut.nextPage('1', 40).subscribe(expectDidNOTHideLoader)
            sut.nextPage('2', 50).subscribe(expectDidHideLoader)
            testDone(done)
        })
    
        it('search, search', function (done) {
            sut.search('1', 60).subscribe(expectDidNOTHideLoader) //even if it takes more time than search2
            sut.search('2', 50).subscribe(expectDidHideLoader)
            testDone(done)
        })
    
        it('search, search - reverse', function (done) {
            sut.search('1', 40).subscribe(expectDidNOTHideLoader) 
            sut.search('2', 50).subscribe(expectDidHideLoader)
            testDone(done)
        })
    
        it('search, nextPage, search', function (done) {
            sut.search('1', 40).subscribe(expectDidNOTHideLoader) //even if it takes more time than search2
            sut.nextPage('1', 30).subscribe(expectDidNOTHideLoader) //even if it takes more time than search2
            sut.search('2', 10).subscribe(expectDidHideLoader)
            testDone(done)
        })
    
        it('search, nextPage (call after response from search)', function (done) {
            sut.search('1', 10).subscribe(result => {
                expectDidHideLoader(result)
                sut.nextPage('1', 10).subscribe(expectDidHideLoader)
            })
            testDone(done)   
        })
    
        function expectDidNOTHideLoader(result){
            expect(result).to.be.false
        }
    
        function expectDidHideLoader(result){
            expect(result).to.be.true
        }
    
        function testDone(done){
            setTimeout(function(){
                done()
            }, 200)
        }
    
    })
    

    部分输出:

    enter image description here

    JSFiddle是here.

答案 3 :(得分:1)

我认为这是一个更简单的解决方案,为了解释它,我希望重新说明&#34;您在编辑中提供的示例:

  1. 状态为&#34;待定&#34;只要有未公开的请求。
  2. 响应会关闭之前的所有请求。
  3. 或者,以流/大理石的方式

    (O =请求[打开],C =响应[关闭],p =挂起,x =未挂起)

    http stream:------ O --- O --- O --- C --- O --- C --- O --- O --- C --- O- -

    ------状态:x ---- P -------------- x --- P ---- x ---- P ---- ----- --- X --- p

    你可以看到计数并不重要,我们有一个标志,它实际上是(待定)或关闭(返回一个响应)。这是因为您使用switchMap / flatMap,或者正如您在编辑结束时所说的那样,每次只有一个活动请求。

    该标志实际上是一个布尔可观察/主观点,或者只是一个主题。

    所以,你需要的是先定义:

    var hasPending: Subject<boolean> = BehaviorSubject(false);
    

    BehaviorSubject有两个原因:

    1. 您可以设置初始值(false =无等待)。
    2. 新订阅者获取最后一个值,因此,即使是稍后创建的组件也会知道您是否有待处理的请求。
    3. 比其余部分变得简单,无论何时发送请求,都要将待处理设置为“真实”,当请求完成时,将待处理标记设置为“假”&#39;。< / p>

      var pageStream = Rx.createObservableFunction(_self, 'nextPage')
          .startWith(1)
          .do(function(pageNumber) {
              hasPending.next(true);
          })
          .concatMap(function(pageNumber) {
              return MyHTTPService.getPage(pageNumber);
          })
          .do(function(response) {
              hasPending.next(false);
          });
      

      Rx.createObservableFunction(_self,&#39; search&#39;)         .flatMapLatest(function(e){             return pageStream;         })         .subscribe();

      这是rxjs 5语法,对于rxjs 4使用onNext(...)

      如果你不需要你的公寓作为一个可观察的,只是一个值,只需声明:

      var hasPending: booolean = false;
      

      然后在http调用之前的.do中执行

      hasPending = true;
      

      并在http调用后的.do中执行

      hasPending = false;
      

      就是这样: - )

      顺便说一句,在重新阅读完所有内容后,您可以通过更简单(尽管有些快速和肮脏)的解决方案来测试: 更改您的帖子http&#39; do&#39;致电:

      .do(function(response) {
              pendingRequests = 0;
          });