我正在玩Reactive Programming,使用RxJS,偶然发现了一些我不确定如何解决的问题。
我们说我们实现了自动售货机。您插入硬币,选择一个项目,机器分配一个项目并返回更改。我们假设价格总是1美分,所以插入四分之一(25美分)应该会返回24美分,依此类推。
"棘手"部分是我希望能够处理用户插入2个硬币然后选择项目的情况。或者在不插入硬币的情况下选择项目。
将插入的硬币和选定的项目实现为流似乎很自然。然后我们可以在这两个动作之间引入某种依赖 - 合并或压缩或组合最新动作。
然而,我很快就遇到了一个问题,我希望积累硬币直到分配物品,但不能进一步分配。 AFAIU,这意味着我无法使用sum
或scan
,因为我无法重置"重置"以前的积累在某个时刻。
这是一个示例图表:
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个值,然后再将其与第二个流合并?
答案 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] 强>
说明:
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.js
和selectedStream
,我们不希望在我们构建查询并且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()
);