问题
As an exercise I'm trying to build 2 dependent streams which update one another.
The test application is simply an "Inches <-> Centimeters" converter, with both inputs editable.
The issue I am experiencing is that I cannot get how can I stop recursion that causes one field change.
To better explain the issue let's have a look at the relevant part of code:
var cmValue = new Rx.BehaviorSubject(0),
inValue = new Rx.BehaviorSubject(0);
# handler #1
cmValue.distinctUntilChanged().subscribe(function(v) {
inValue.onNext(cmToIn(v));
});
# handler #2
inValue.distinctUntilChanged().subscribe(function (v) {
cmValue.onNext(inToCm(v));
});
So we define to Subjects each of which holds the current corresponding value.
Now imagine we change the value in inches to 2
(using inValue.onNext(2);
or via keyboard).
What happens next - is the handler #2 is triggered and it invokes a corresponding recalculation of a value in centimeters. Which results to cmValue.onNext(0.7874015748031495)
.
This call in fact is then handled by handler #1 and causes the value in inches (the one we put manually) to be recalculated, using 0.7874015748031495 * 2.54
formula which causes another inValue.onNext(1.99999999999999973)
call.
Luckily - due to FP rounding error that's where we stop. But in other scenarios this may lead to more loops or even to an infinite recursion.
As you can see - I partially solved the issue applying .distinctUntilChanged()
which at least protects us from an infinite recursion on any change, but as we can see - in this case it's does not solve the problem entirely since values are not identical (due to FP operations nature).
So the question is: how would one implement a generic two-way binding that does not cause self-recursion at all?
I emphasized generic to make a note that using .select()
with rounding would be a partial solution for this particular issue, and not the generic one (which I and everyone else would prefer).
The complete code and demo: http://jsfiddle.net/ewr67eLr/
回答1:
In your demo you have two input fields. "Keyup" events for this inputs will be the information source, and inputs value will be destination. In this case you don't need mutable states for checking updates of observables.
Rx.Observable.fromEvent(cmElement, 'keyup')
.map(targetValue)
.distinctUntilChanged()
.map(cmToIn)
.startWith(0)
.subscribe(function(v){ inElement.value = v; });
Rx.Observable.fromEvent(inElement, 'keyup')
.map(targetValue)
.distinctUntilChanged()
.map(inToCm)
.startWith(0)
.subscribe(function(v){ cmElement.value = v; });
Check my example here: http://jsfiddle.net/537Lrcot/2/
回答2:
I had a similar task to solve on my project.
First, you have to select your ground truth - the value that is going to represent your measurement, let's say you choose centimeters.
Now you're working with only one stream that is getting updates from several sources.
Since have to store a value that cannot be represented precisely as you input it, you have to output it with fewer significant digits than the whole float. A person is not likely to measure inches to the precision of 11 significant digits, so there is no point to display converted value to that precision.
function myRound(x, digits) {
var exp = Math.pow(10, digits);
return Math.round(x * exp) / exp;
}
cmValue.subscribe(function(v) {
if (document.activeElement !== cmElement) {
cmElement.value = myRound(v, 3).toFixed(3);
}
if (document.activeElement !== inElement) {
inElement.value = myRound(cmToIn(v), 3).toFixed(3);
}
});
So far, here's a working example: http://jsfiddle.net/ewr67eLr/4/
What's left is an edge case when we changed our focus to auto-computed value and then that value recalculates our first value with different digits.
This can be solved by creating streams for values that have been changed by user:
cmInputValue = new Rx.BehaviorSubject(0),
inInputValue = new Rx.BehaviorSubject(0),
...
Rx.Observable.fromEvent(cmElement, 'input').subscribe(function (e) {
cmInputValue.onNext(e.target.value);
});
Rx.Observable.fromEvent(inElement, 'input').subscribe(function (e) {
inInputValue.onNext(e.target.value);
});
cmInputValue.distinctUntilChanged().subscribe(function (v) {
cmValue.onNext(v);
});
inInputValue.distinctUntilChanged().subscribe(function (v) {
cmValue.onNext(inToCm(v));
});
http://jsfiddle.net/ewr67eLr/6/
Now this is the best way I could solve this task.
回答3:
Here's a purely algorithmic way to solve the problem (ie it doesn't rely upon the specific state of the DOM). Basically just use a variable to detect recursion and abort the update.
/* two way binding */
var twowayBind = function (a, b, aToB, bToA) {
var updatingA = 0,
updatingB = 0,
subscribeA = new Rx.SingleAssignmentDisposable(),
subscribeB = new Rx.SingleAssignmentDisposable(),
subscriptions = new Rx.CompositeDisposable(subscribeA, subscribeB);
subscribeA.setDisposable(a.subscribe(function (value) {
if (!updatingB) {
++updatingA;
b.onNext(aToB(value));
--updatingA;
}
}));
subscribeB.setDisposable(b.subscribe(function (value) {
if (!updatingA) {
++updatingB;
a.onNext(bToA(value));
--updatingB;
}
});
return subscriptions;
};
var cmValue = new BehavoirSubject(0),
inValue = new BehaviorSubject(0),
binding = twowayBind(cmValue, inValue, cmToIn, inToCm);
回答4:
As noted in my comment, this problem doesn't require a loop. It also doesn't require Subjects, or document.activeElement. You can have events from input A update B, and events from input B update A without streams referring to each other.
Example here:
http://jsfiddle.net/kcv15h6p/1/
relevant bits here:
var cmElement = document.getElementById('cm'),
inElement = document.getElementById('in'),
cmInputValue = Rx.Observable.fromEvent(cmElement, 'input').map(evToValue).startWith(0),
inInputValue = Rx.Observable.fromEvent(inElement, 'input').map(evToValue).startWith(0);
inInputValue.map(inToCm).subscribe(function (v) {
cmElement.value = myRound(v, 3).toFixed(3);
});
cmInputValue.map(cmToIn).subscribe(function (v) {
inElement.value = myRound(v, 3).toFixed(3);
});
For problems that really require loops, you can create loops with defer, as pointed out in Brandon's answer to this question:
Catch circular dependency between observables
Like any looping construct, you have to handle the exit condition to avoid an infinite loop. You can do this with operators like take(), or distinctUntilChanged(). Note that the latter takes a comparator, so you can, for example, use object identity (x, y) => x === y, to exit a loop.
来源:https://stackoverflow.com/questions/27596693/safe-update-for-2-dependent-streams