【问题标题】:Reactive Programming with RxJS - can this scroll function be simplified?使用 RxJS 进行反应式编程 - 这个滚动功能可以简化吗?
【发布时间】:2015-07-28 00:46:31
【问题描述】:

我对响应式编程(和 RxJS)还很陌生,所有这些运算符都很难理解。

不管怎样,我已经成功编写了这个函数,它可以在拖动某些东西时处理文档的滚动。我现在想知道这是否可以简化。

基本上,onMouseDown 我需要每 10 毫秒检查一次鼠标的位置,并且当鼠标移动时我需要更新的 clientY,这就是为什么我设置了一个 Rx.Oberservable.interval(10),我将它与 mouseMove 观察器结合使用。无论您是否移动鼠标(如预期的那样),这都会滚动页面。

代码如下:

handleWindowScrollOnDrag() {
    var dragTarget = this.getDOMNode()
    var scrollTarget = document.body
    var wHeight = window.innerHeight
    var maxScroll = document.documentElement.scrollHeight - wHeight

    // Get the three major events
    var mouseup   = Rx.Observable.fromEvent(document, 'mouseup');
    var mousemove = Rx.Observable.fromEvent(document, 'mousemove');
    var mousedown = Rx.Observable.fromEvent(dragTarget, 'mousedown');

    var mousedrag = mousedown.flatMap(function (md) {
        var y = scrollTarget.scrollTop
        var multiplier = 1

        // Scroll every 10ms until mouseup when we can
        var intervalSource = Rx.Observable.interval(10).takeUntil(mouseup);

        // Get actual clientY until mouseup
        var movement = mousemove.map(function (mm) {
            return {
                y: mm.clientY
            };
        }).takeUntil(mouseup);

        return Rx.Observable
            .combineLatest(movement, intervalSource, function (s1) {
                multiplier = 1

                if (s1.y < 100 && y >= 0) {
                    if (s1.y < 75) multiplier = 3;
                    if (s1.y < 50) multiplier = 5;
                    if (s1.y < 25) multiplier = 10;
                    if (s1.y < 15) multiplier = 20;
                    y -= (1 * multiplier)
                }

                if (s1.y > wHeight - 100 && y <= (maxScroll)) {
                    if (s1.y > wHeight - 75) multiplier = 3;
                    if (s1.y > wHeight - 50) multiplier = 5;
                    if (s1.y > wHeight - 25) multiplier = 10;
                    if (s1.y > wHeight - 15) multiplier = 20;
                    y += (1 * multiplier)
                }

                return {
                    y: y
                };
        });
    });

    // Update position
    this.subscription = mousedrag.subscribe(function (pos) {
        document.body.scrollTop = pos.y
    });

},

【问题讨论】:

    标签: javascript functional-programming reactive-programming rxjs


    【解决方案1】:

    您可以删除在movement 流中只需要一个的额外takeUntils,因为combineLatest 运算符将在完成时清理并处置所有基础订阅。

    接下来,您可以通过使用.scan 来管理累积状态而不是闭包变量来删除一些额外的状态。

    您还可以通过简单地传递鼠标移动的y 值来简化事件主体,而不是在mapcombineLatest 运算符中产生对象分配的开销。

    最后,我会改为使用.withLatestFrom 而不是combineLatest。由于您已经基本上以 100 fps 的速度轮询,间隔 combineLatest 如果鼠标同时移动,则发射速度会更快。

    附带说明一下,虽然我对 DOM 滚动的工作原理并不十分熟悉(而且我实际上不知道您的代码在野外的工作情况如何),但向页面的滚动值发送垃圾邮件的速度比页面似乎有点矫枉过正。降低间隔率并使用jQuery.animate 之类的东西可能会更好地平滑其间的滚动。

    ;tldr

    handleWindowScrollOnDrag() {
      var dragTarget = this.getDOMNode()
      var scrollTarget = document.body;
      var wHeight = window.innerHeight;
      var maxScroll = document.documentElement.scrollHeight - wHeight;
    
      // Get the three major events
      var mouseup   = Rx.Observable.fromEvent(document, 'mouseup');
      var mousemove = Rx.Observable.fromEvent(document, 'mousemove');
      var mousedown = Rx.Observable.fromEvent(dragTarget, 'mousedown');
    
      var mousedrag = mousedown.flatMap(function (md) {
          // Scroll every 10ms until mouseup when we can
          var intervalSource = Rx.Observable.interval(10, Rx.Scheduler.requestAnimationFrame);
    
          return intervalSource
              .takeUntil(mouseup)
              .withLatestFrom(mousemove, function (s1, s2) {
                  return s2.clientY;
               })
               .scan(scrollTarget.scrollTop, function(y, delta) { 
                  var multiplier = 1;
    
                  if (delta  < 100 && y >= 0) {
                      if (delta < 75) multiplier = 3;
                      if (delta < 50) multiplier = 5;
                      if (delta < 25) multiplier = 10;
                      if (delta < 15) multiplier = 20;
                      y -= (1 * multiplier);
                  }
    
                  if (delta > wHeight - 100 && y <= (maxScroll)) {
                      if (delta > wHeight - 75) multiplier = 3;
                      if (delta > wHeight - 50) multiplier = 5;
                      if (delta > wHeight - 25) multiplier = 10;
                      if (delta > wHeight - 15) multiplier = 20;
                      y += (1 * multiplier);
                  }
    
                  return y;
            });
      });
    
      // Update position
      this.subscription = mousedrag.subscribe(function (pos) {
          document.body.scrollTop = pos;
      });
    
    },
    

    编辑

    原文中的一个错误实际上可以进一步简化代码。您可以通过将mousemove 传递给withLatestFrom 并使用该结果选择器获取clientY 来一起删除movement 流(以及对map 的额外调用)

    此外,如果您尝试将interval 与页面的渲染循环同步,我还添加了您可能想要添加调度程序的位置,尽管我仍然会说您可能想要使用 15 毫秒(60 fps)而不是 10 毫秒,它再次归结为滚动渲染是如何完成的。

    如果设置滚动位置只会在每个渲染循环结束时更新,那么它将正常工作。但是,如果它在每个 set 之后贪婪地重新计算,那么最好有一个像 React 这样的中介先写入虚拟 DOM,而不是将所有更改直接刷新到 DOM。

    【讨论】:

    • 非常感谢您的解释和提示!我已经尝试了您建议的代码,但其中一部分很奇怪,var movement = mousemove.map(function (mm) { return mm.clientY; }); 这实际上仍然返回整个MouseEvent。我不得不将上面写着return s2 的部分更改为return s2.clientY。知道为什么吗?否则它会起作用:) 关于 requestAnimationFrame 优化:我已经有了同样的想法,我看到有一个Rx.Scheduler.requestAnimationFrame。但我不确定如何添加它。
    • @MartinBroder 好收获!你是对的,这是因为我仍在将mousemove 传递给withLatestFrom。事实证明这实际上更可取,因为您可以使用 withLatestFrom 中的 selector 函数并为自己节省额外的 map 操作。编辑了这个和一些关于调度的要点。
    • 嘿@paulpdaniels,我感激不尽!玩得更多,当我将它添加到 intervalSource 时,看起来 .observeOn(Rx.Sched...frame) 也一样,对吗?据我了解,它会覆盖源调度程序。我认为你这样做的方式更短,对吗?
    • @MartinBroder 非常欢迎您! observerOn 实际上 重新安排 事件,它们仍然会以同样快的速度生成,因为它不会取代原来的 scheduler。在这种使用interval 的情况下,通过添加scheduler 参数,您可以控制生成 的速度。同样interval 使用递归调度,它将占用更小的内存(尽管在大多数情况下这是一个小问题)。
    猜你喜欢
    • 2021-10-13
    • 2013-12-22
    • 2015-04-02
    • 2022-01-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-23
    • 2017-07-01
    相关资源
    最近更新 更多