累积和重置流

时间:2015-11-06 13:07:20

标签: javascript asynchronous reactive-programming rxjs

我正在玩Reactive Programming,使用RxJS,偶然发现了一些我不确定如何解决的问题。

我们说我们实现了自动售货机。您插入硬币,选择一个项目,机器分配一个项目并返回更改。我们假设价格总是1美分,所以插入四分之一(25美分)应该会返回24美分,依此类推。

"棘手"部分是我希望能够处理用户插入2个硬币然后选择项目的情况。或者在不插入硬币的情况下选择项目。

将插入的硬币和选定的项目实现为流似乎很自然。然后我们可以在这两个动作之间引入某种依赖 - 合并或压缩或组合最新动作。

然而,我很快就遇到了一个问题,我希望积累硬币直到分配物品,但不能进一步分配。 AFAIU,这意味着我无法使用sumscan,因为我无法重置"重置"以前的积​​累在某个时刻。

这是一个示例图表:

coins: ---25---5-----10------------|->
acc:   ---25---30----40------------|->
items: ------------foo-----bar-----|->
combined: ---------30,foo--40,bar--|->
change:------------29------39------|->

和相应的代码:

this.getCoinsStream()
  .scan(function(sum, current) { return sum + current })
  .combineLatest(this.getSelectedItemsStream())
  .subscribe(function(cents, item) {
    dispenseItem(item);
    dispenseChange(cents - 1);
  });

插入25美分和5美分然后" foo"项目已被选中。积累硬币然后结合最新将导致" foo"与" 30"结合(这是正确的)然后" bar"用" 40" (这是不正确的;应该是" bar"和" 10")。

我浏览了all of the methods进行分组和过滤,并且看不到任何可以使用的内容。

我可以使用的替代解决方案是分别累积硬币。但是这会引入流之外的状态,我真的想避免这种情况:

var centsDeposited = 0;
this.getCoinsStream().subscribe(function(cents) {
  return centsDeposited += cents;
});

this.getSelectedItemsStream().subscribe(function(item) {
  dispenseItem(item);
  dispenseChange(centsDeposited - 1);
  centsDeposited = 0;
});

此外,这并不允许使流相互依赖,例如等待硬币插入,直到所选动作可以返回项目。

我错过了现有的方法吗?什么是实现这样的事情的最佳方式 - 累积值直到需要与另一个流合并的时刻,还要在第一个流中等待至少1个值,然后再将其与第二个流合并?

3 个答案:

答案 0 :(得分:2)

您可以使用您的scan / combineLatest方法,然后使用first完成流跟踪repeat,以便它“重新开始”流,但观察者不会看到它。< / p>

var coinStream = Rx.Observable.merge(
    Rx.Observable.fromEvent($('#add5'), 'click').map(5),
    Rx.Observable.fromEvent($('#add10'), 'click').map(10),
    Rx.Observable.fromEvent($('#add25'), 'click').map(25)
    );
var selectedStream = Rx.Observable.merge(
  Rx.Observable.fromEvent($('#coke'), 'click').map('Coke'),
  Rx.Observable.fromEvent($('#sprite'), 'click').map('sprite')
);

var $selection = $('#selection');
var $change = $('#change');

function dispense(selection) {
 $selection.text('Dispensed: ' + selection); 
 console.log("Dispensing Drink: " + selection);   
}

function dispenseChange(change) {
 $change.text('Dispensed change: ' + change); 
 console.log("Dispensing Change: " + change);   
}


var dispenser = coinStream.scan(function(acc, delta) { return acc + delta; }, 0)
                              .combineLatest(selectedStream, 
                                 function(coins, selection) {
                                   return {coins : coins, selection : selection};
                                 })

                              //Combine latest won't emit until both Observables have a value
                              //so you can safely get the first which will be the point that
                              //both Observables have emitted.
                              .first()

                              //First will complete the stream above so use repeat
                              //to resubscribe to the stream transparently
                              //You could also do this conditionally with while or doWhile
                              .repeat()

                              //If you only will subscribe once, then you won't need this but 
                              //here I am showing how to do it with two subscribers
                              .publish();

    
    //Dole out the change
    dispenser.pluck('coins')
             .map(function(c) { return c - 1;})
             .subscribe(dispenseChange);
    
    //Get the selection for dispensation
    dispenser.pluck('selection').subscribe(dispense);
    
    //Wire it up
    dispenser.connect();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.0.6/rx.all.js"></script>
<button id="coke">Coke</button>
<button id="sprite">Sprite</button>
<button id="add5">5</button>
<button id="add10">10</button>
<button id="add25">25</button>
<div id="change"></div>
<div id="selection"></div>

答案 1 :(得分:1)

一般来说,你有以下方程组:

inserted_coins :: independent source
items :: independent source
accumulated_coins :: sum(inserted_coins)
accumulated_paid :: sum(price(items))
change :: accumulated_coins - accumulated_paid
coins_in_machine :: when items : 0, when inserted_coins : sum(inserted_coins) starting after last emission of item

困难的部分是coins_in_machine。您需要根据两个来源的一些排放来切换源可观察量。

function emits ( who ) {
  return function ( x ) { console.log([who, ": "].join(" ") + x);};
}

function sum ( a, b ) {return a + b;}

var inserted_coins = Rx.Observable.fromEvent(document.getElementById("insert"), 'click').map(function ( x ) {return 15;});
var items = Rx.Observable.fromEvent(document.getElementById("item"), 'click').map(function ( x ) {return "snickers";});

console.log("running");

var accumulated_coins = inserted_coins.scan(sum);

var coins_in_machine =
    Rx.Observable.merge(
        items.tap(emits("items")).map(function ( x ) {return {value : x, flag : 1};}),
        inserted_coins.tap(emits("coins inserted ")).map(function ( x ) {return {value : x, flag : 0};}))
        .distinctUntilChanged(function(x){return x.flag;})
        .flatMapLatest(function ( x ) {
                   switch (x.flag) {
                     case 1 :
                       return Rx.Observable.just(0);
                     case 0 :
                       return inserted_coins.scan(sum, x.value).startWith(x.value);
                   }
                 }
    ).startWith(0);

coins_in_machine.subscribe(emits("coins in machine"));

jsbin:http://jsbin.com/mejoneteyo/edit?html,js,console,output

<强> [UPDATE]

说明:

  • 我们将insert_coins流与items流合并,同时为它们附加一个标志,以便在我们在合并流中收到值时发出两个中的哪一个
  • 当项目流发出时,我们想在coins_in_machine中加0。如果是insert_coins我们想要对输入值求和,因为该总和将代表机器中新的硬币数量。这意味着insert_coins的定义在之前定义的逻辑下从一个流切换到另一个流。该逻辑是switchMapLatest
  • 中实现的
  • 我使用switchMapLatest而不是switchMap,否则coins_in_machine流将继续接收来自前切换流的发射,即重复发射,因为最终只有两个流我们切换到的。如果可以的话,我会说这是我们需要的关闭和转换。
  • switchMapLatest必须返回一个流,因此我们跳过箍来制作一个发出0和never的流结束(并且不会阻止计算机,因为使用repeat运算符在那种情况下)
  • 我们跳过一些额外的箍,让inserted_coins发出我们想要的值。我的第一个实现是inserted_coins.scan(sum,0),但从未奏效。关键和我发现相当棘手的是,当我们到达流程中的那一点时,inserted_coins已经发出了作为总和的一部分的一个值。该值是作为flatMapLatest的参数传递的值,但它不再在源中,因此在事实之后调用scan得到它,因此有必要从中获取该值flatMapLatest并重新构建正确的行为。

答案 2 :(得分:0)

您还可以使用Window将多个硬币事件组合在一起,并使用项目选择作为窗口边界。

接下来我们可以使用zip来获取项目值。

请注意,我们会立即尝试发布项目。因此,用户必须在决定项目之前插入硬币。

注意我出于安全原因决定发布Express.jsselectedStream,我们不希望在我们构建查询并且zip变得不平衡时导致事件触发的竞争条件。这将是一个非常罕见的情况,但请注意,当我们的消息来源是冷Observables时,他们几乎在我们订阅时就开始生成,我们必须使用dispenser来保护自己。

(无耻地偷走了paulpdaniels示例代码)。

Publish
var coinStream = Rx.Observable.merge(
    Rx.Observable.fromEvent($('#add5'), 'click').map(5),
    Rx.Observable.fromEvent($('#add10'), 'click').map(10),
    Rx.Observable.fromEvent($('#add25'), 'click').map(25)
);

var selectedStream = Rx.Observable.merge(
    Rx.Observable.fromEvent($('#coke'), 'click').map('Coke'),
    Rx.Observable.fromEvent($('#sprite'), 'click').map('Sprite')
).publish();

var $selection = $('#selection');
var $change = $('#change');

function dispense(selection) {
    $selection.text('Dispensed: ' + selection); 
    console.log("Dispensing Drink: " + selection);   
}

function dispenseChange(change) {
    $change.text('Dispensed change: ' + change); 
    console.log("Dispensing Change: " + change);   
}

// Build the query.
var dispenser = Rx.Observable.zip(
    coinStream
        .window(selectedStream)
        .flatMap(ob => ob.reduce((acc, cur) => acc + cur, 0)),
    selectedStream,
    (coins, selection) => ({coins : coins, selection: selection})
).filter(pay => pay.coins != 0) // Do not give out items if there are no coins.
.publish();

var dispose = new Rx.CompositeDisposable(
    //Dole out the change
    dispenser
        .pluck('coins')
        .map(function(c) { return c - 1;})
        .subscribe(dispenseChange),

    //Get the selection for dispensation
    dispenser
        .pluck('selection')
        .subscribe(dispense),

    //Wire it up
    dispenser.connect(),
    selectedStream.connect()
);