【问题标题】:Safe update for 2 dependent streams2 个依赖流的安全更新
【发布时间】:2014-12-22 04:43:47
【问题描述】:

作为一项练习,我正在尝试构建 2 个相互更新的依赖流。

测试应用程序只是一个“英寸 厘米”转换器,两个输入均可编辑。

我遇到的问题是我无法理解如何停止导致一个字段更改的递归。

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

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

所以我们给 Subjects 定义了每一个都持有当前对应的值。

现在假设我们以英寸为单位将值更改为 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/

【问题讨论】:

  • 从问题中不清楚为什么需要递归。您可以让来自输入 A 的事件更新 B,来自输入 B 的事件更新 A 而无需流相互引用。 IE。分配给 cmElement.value 不会生成事件,因此没有循环。例如jsfiddle.net/kcv15h6p/1 你在这里创建了一个循环,通过引入不需要解决所述问题的连接主题。但是,您可以在需要时在 Rx 中安全地创建循环。也许用需要循环的问题来更新问题。
  • @user1009908:我不需要需要递归。我的问题是如何消除它。 “也许用需要循环的问题来更新问题”---问题已经描述过了,抱歉。我喜欢你的解决方案。热衷于将其作为答案?

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


【解决方案1】:

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

首先,您必须选择您的基本事实 - 将代表您的测量值的值,假设您选择厘米。

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

由于必须存储一个无法在您输入时精确表示的值,因此您必须以比整个浮点数更少的有效数字输出它。一个人不可能以 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/

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

【讨论】:

  • “首先,你必须选择你的基本事实”——确切地说,就像在最初的实现中那样,然后我故意将其更改为没有,以便答案不偏不倚: -) 进一步阅读答案...)
  • 已彻底阅读。是的,它与我猜想的差不多。老实说document.activeElement 把戏看起来很脏。我的意思是,我希望 RxJS 能够以更好的方式设计这种应用程序。
  • 据我所知,您正在尝试做的事情没有“好的”数学表示,所以它要么丑陋要么复杂。
  • 我不明白为什么这里需要主题。此外,转换 fns 是相反的。 ;) 这不是吗? jsfiddle.net/kcv15h6p/1
【解决方案2】:

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

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】:

    这是解决问题的纯算法方法(即它不依赖于 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);
    

    【讨论】:

    • 是的,我的替代原始解决方案类似:我想创建一个共享流,如果您需要传播递归,它将携带一个具有值和标志的对象
    【解决方案4】:

    正如我在评论中提到的,这个问题不需要循环。它也不需要主题或 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);
    });
    

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

    Catch circular dependency between observables

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

    【讨论】:

      猜你喜欢
      • 2020-04-30
      • 1970-01-01
      • 1970-01-01
      • 2017-01-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-24
      • 1970-01-01
      相关资源
      最近更新 更多