2个依赖流的安全更新

时间:2014-12-22 04:43:48

标签: javascript reactive-programming rxjs reactive-extensions-js

作为练习,我正在尝试构建彼此更新的2个依赖流。

测试应用程序只是一个“Inches< - > Centimeters”转换器,两个输入都是可编辑的。

我遇到的问题是我无法得到如何阻止导致一次字段更改的递归。

为了更好地解释这个问题,让我们看一下代码的相关部分:

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));
});

因此我们定义了主题,每个主题都包含当前对应的值。

现在假设我们将以英寸为单位的值更改为2(使用inValue.onNext(2);或通过键盘)。

接下来会发生什么 - 处理程序#2被触发,它会以厘米为单位调用相应的值重新计算。结果为cmValue.onNext(0.7874015748031495)

这个调用实际上由处理程序#1处理,并使用导致另一个0.7874015748031495 * 2.54调用的inValue.onNext(1.99999999999999973)公式重新计算以英寸为单位的值(我们手动放置的值)。

幸运的是 - 由于FP舍入错误,我们停止了。但在其他情况下,这可能会导致更多循环甚至无限递归。

正如您所看到的 - 我部分地解决了应用.distinctUntilChanged()的问题,这至少可以保护我们免受任何更改的无限递归,但正如我们所看到的 - 在这种情况下,它不能完全解决问题,因为值不相同(由于FP操作性质)。

所以问题是:如何实现一个不会导致自递归的泛型双向绑定?

我强调泛型要注意,使用带有舍入的.select()将是这个特定问题的部分解决方案,而不是通用的(我和其他人都喜欢的)。

完整的代码和演示:http://jsfiddle.net/ewr67eLr/

4 个答案:

答案 0 :(得分:2)

我的项目有类似的任务要解决。

首先,你必须选择你的基本事实 - 代表你的测量的价值,让我们说你选择厘米。

现在您只使用了一个从多个来源获取更新的流。

由于必须存储一个不能在输入时精确表示的值,因此必须使用比整个浮点数更少的有效数字输出它。一个人不太可能以11位有效数字的精度测量英寸,因此没有必要将转换后的值显示为该精度。

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);
    }
});

到目前为止,这是一个有效的例子:http://jsfiddle.net/ewr67eLr/4/

当我们将焦点更改为自动计算值然后该值重新计算不同数字的第一个值时,剩下的是边缘情况。

这可以通过为用户更改的值创建流来解决:

    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/

现在这是我解决此任务的最佳方式。

答案 1 :(得分:1)

这是一种解决问题的纯粹算法方法(即它不依赖于DOM的特定状态)。基本上只需使用变量来检测递归并中止更新。

/* 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);

答案 2 :(得分:1)

在您的演示中,您有两个输入字段。此输入的“Keyup”事件将是信息源,输入值将是目标。在这种情况下,您不需要可变状态来检查observable的更新。

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; });

点击此处查看我的示例:http://jsfiddle.net/537Lrcot/2/

答案 3 :(得分:1)

正如我的评论所述,这个问题并不需要循环。它也不需要Subjects或document.activeElement。您可以从输入A更新B中获取事件,并从输入B更新A中的事件,而不使用相互引用的流。

此处示例:

http://jsfiddle.net/kcv15h6p/1/

相关位:

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);
});

对于真正需要循环的问题,你可以使用延迟创建循环,正如Brandon对这个问题的回答所指出的那样:

Catch circular dependency between observables

与任何循环结构一样,您必须处理退出条件以避免无限循环。您可以使用take()或distinctUntilChanged()等运算符执行此操作。请注意,后者采用比较器,因此您可以使用对象标识(x,y)=> x === y,退出循环。