检测滚动事件是否由用户创建

111

有没有办法区分滚动事件是由浏览器还是用户引起的?特别是使用“后退”按钮时,浏览器可能会跳转到上次滚动位置。如果我绑定了滚动事件,如何判断滚动是由用户还是浏览器引起的?

$(document).scroll( function(){ 
    //who did this?!
});

我看到有三种情况会导致浏览器滚动。

  1. 用户执行某些操作,例如使用鼠标滚轮、箭头键、上下页键、Home/End键、点击滚动条或拖动其拇指。
  2. 浏览器自动滚动。例如,在浏览器中使用后退按钮时,它会自动跳转到最后已知的滚动位置。
  3. JavaScript 滚动。例如,element.scrollTo(x,y)

1
从你的问题中我不确定你是否认为使用后退按钮跳转到我的页面是浏览器或用户的滚动事件。一般来说,你认为“浏览器滚动”是什么意思?如果你指的是由你的脚本发起的滚动,那么你需要做的就是当你的脚本滚动时,要么停用事件处理程序,要么设置一个标志,以便事件处理程序知道忽略它。 - RoToRa
我认为通过后退按钮滚动是“浏览器滚动”。其他任何方式 - 鼠标滚轮,上/下箭头,中间按钮单击等都将是用户滚动。我想我的真正问题可能是 - 有没有办法区分事件来自哪里?我确实查看了事件对象上的属性,但找不到任何东西。我能想象的三种情况是浏览器启动的滚动、JavaScript 启动的滚动和用户启动的滚动。希望这样能让事情更清晰。 - mrtsherman
@mrtsherman 我在实现相同输出时发现了一些东西:https://dev59.com/GXE85IYBdhLWcg3wUB2z - talha2k
11个回答

36
很可惜,没有直接的方法可以判断这一点。 我建议如果您能重新设计应用程序,使其不依赖于此类流程,请这样做。否则,我能想到的一个解决方法是跟踪用户启动的滚动,并检查它以查看滚动是由浏览器还是用户触发的。 这是我编写的一个示例,它做得很好(除了在 jQuery 历史记录有问题的浏览器中)。 您需要在本地运行此代码才能完全测试它(jsFiddle / jsbin 不适合,因为它们会将内容放入 iFrame 中)。这是我验证的测试用例:页面加载-userScrollfalse;使用鼠标/键盘滚动-userScroll变为true;单击链接跳转到页面底部-userScroll变为false;单击“后退/前进”-userScroll变为false
<!DOCTYPE html> 
<html lang="en"> 
<head> 
    <meta charset="utf-8" /> 
    <script src="http://code.jquery.com/jquery-1.6.1.min.js"></script> 
    <script type="text/javascript" src="https://raw.github.com/tkyk/jquery-history-plugin/master/jquery.history.js"></script> 
</head> 
<body> 
    <span> hello there </span><br/> 
    <a href="#bottom"> click here to go down </a> 
    <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> 
    <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> 
    <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> 
    <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> 
    <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> 
    <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> 
    <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> 
    <a name="bottom"> just sitting </a> 
</body> 
<script type="text/javascript"> 

var userScroll = false;     

function mouseEvent(e) { 
    userScroll = true; 
} 


$(function() { 

    // reset flag on back/forward 
    $.history.init(function(hash){ 
        userScroll = false; 
    }); 

    $(document).keydown(function(e) { 
        if(e.which == 33        // page up 
           || e.which == 34     // page dn 
           || e.which == 32     // spacebar
           || e.which == 38     // up 
           || e.which == 40     // down 
           || (e.ctrlKey && e.which == 36)     // ctrl + home 
           || (e.ctrlKey && e.which == 35)     // ctrl + end 
          ) { 
            userScroll = true; 
        } 
    }); 

    // detect user scroll through mouse
    // Mozilla/Webkit 
    if(window.addEventListener) {
        document.addEventListener('DOMMouseScroll', mouseEvent, false); 
    }

    //for IE/OPERA etc 
    document.onmousewheel = mouseEvent; 


    // to reset flag when named anchors are clicked
    $('a[href*=#]').click(function() { 
        userScroll = false;
    }); 

      // detect browser/user scroll
    $(document).scroll( function(){  
        console.log('Scroll initiated by ' + (userScroll == true ? "user" : "browser"));
    });
}); 
</script> 
</html>

注意事项:

  • 当用户使用鼠标拖动滚动条时,此代码不会跟踪滚动。你可以添加一些代码来实现这个功能,我把它留给你作为练习。
  • event.keyCodes 可能因操作系统而异,因此你可能需要相应地进行更改。

希望这有所帮助!


谢谢你,Mrchief。我认为这是最好的答案,尽管它不是我希望的答案(什么?没有event.thrower属性?!)。 - mrtsherman
使用Home和End键进行滚动时,无需按Control键。 - Curtis
DOMMouseScroll在Mac上的最新Chrome上无法工作。最好使用document.addEventListener('mousewheel'而不是设置onmousewheel。 - Curtis

14

与其试图捕捉所有用户事件,更容易的方法是做相反的事情,只处理程序事件并忽略那些用户事件。

例如,以下代码可以工作:

// Element that needs to be scrolled
var myElement = document.getElementById('my-container');

// Flag to tell if the change was programmatic or by the user
var ignoreNextScrollEvent = false;

function setScrollTop(scrollTop) {
    ignoreNextScrollEvent = true;
    myElement.scrollTop = scrollTop
}

myElement.addEventListener('scroll', function() {
    if (ignoreNextScrollEvent) {
        // Ignore this event because it was done programmatically
        ignoreNextScrollEvent = false;
        return;
    }

    // Process user-initiated event here
});

当您调用setScrollTop()时,滚动事件将被忽略,而如果用户使用鼠标、键盘或其他任何方式滚动,则将处理该事件。


10
这不会检测/忽略由浏览器触发的滚动事件。 - mfluehr
1
我一直在考虑类似的事情,需要补充的是,这并没有考虑到滚动行为可以设置为“平滑”。 - Kamil Bęben

9
据我所知,无法在没有任何操作的情况下判断滚动事件是由“用户”还是其他方式触发的。
您可以尝试(类似于其他人提到的)捕获鼠标滚轮事件,然后可能尝试通过检查当前聚焦的元素来捕获可以触发滚动的任何键的keydown事件(箭头,空格等),因为例如在输入字段中输入时无法使用箭头键滚动。总的来说,这将是一个复杂而混乱的脚本。
根据您处理的情况,我想您可以“反转逻辑”,而不是检测用户发出的滚动事件,而是挂钩到通过编程方式进行的任何滚动,并将您代码未创建的任何滚动事件视为用户创建的。就像我所说的那样,这取决于情况和您要实现的目标。

谢谢WTK。这是目前为止最好的答案,但不幸的是,Mrcief用一些代码示例详细阐述了你的答案,所以我把赏金给了他。如果我能分成两份,我会这样做! - mrtsherman
4
没问题。这不是关于赏金,而是关于传播和获取知识 :) - WTK

4

是的,这完全可以。我目前在一个不需要IE的应用程序中使用它——仅面向客户端。当我的Backbone应用程序启动滚动更改动画时,滚动发生但不会触发这些事件捕获。这在最新的Firefox、Safari和Chrome中进行了测试。

$('html, body').bind('scroll mousedown wheel DOMMouseScroll mousewheel keyup', function(evt) {
  // detect only user initiated, not by an .animate though
    if (evt.type === 'DOMMouseScroll' || evt.type === 'keyup' || evt.type === 'mousewheel') {
    // up
    if (evt.originalEvent.detail < 0 || (evt.originalEvent.wheelDelta && evt.originalEvent.wheelDelta > 0)) { 
    // down.
    } else if (evt.originalEvent.detail > 0 || (evt.originalEvent.wheelDelta && evt.originalEvent.wheelDelta < 0)) { 
  }
}
}); 

1
谢谢您的建议。然而,这是一个二元解决方案 - 检测JavaScript启动的滚动与非JS滚动。我真的需要一个三元解决方案(JavaScript、用户、浏览器)。 - mrtsherman

3

谢谢您的建议。然而,我认为这并不是我想要的。我实际上需要区分浏览器滚动和用户滚动。不仅仅是针对鼠标滚轮。 - mrtsherman
我认为鼠标滚轮事件会在滚动事件之前触发。因此,您可以在鼠标滚轮事件中设置var userscroll=true,并在onscroll事件中检测它(并将其重置为false)。 - Gerben
3
有许多用户触发的滚动事件是不包含在内的,如箭头键、窗口调整大小等。说发送此事件的对象是“X”要比说这个事件之外的一切都必须是“X”更为安全。而且,无论如何,这对我也没有用,因为我想识别浏览器触发的滚动,而不是用户触发的滚动。因此,为了实现这个用例,我必须跟踪所有鼠标滚轮滚动事件,然后尝试推断后续的滚动事件是否由用户触发。但我担心这根本行不通。 - mrtsherman
滚轮事件并不一定会触发滚动事件,这取决于上下文。 https://developer.mozilla.org/zh-CN/docs/Web/API/Element/wheel_event - Joyce Lee

2

当页面加载完成时,您可以检查滚动位置。在触发on scroll事件时,请检查滚动位置是否与页面加载时不同。最后,确保一旦页面被滚动,清除存储的值。

$(function () {
    var loadScrollTop = ($(document).scrollTop() > 0 ? $(document).scrollTop() : null);
    $(document).scroll(function (e) {
        if ( $(document).scrollTop() !== loadScrollTop) {
            // scroll code here!
        }
        loadScrollTop = null;
    });
});

谢谢你提供这个想法。对我来说,有一些情况并不完全适用。虽然这是一个好主意,如果我需要的话,我会将其保留在我的后备计划中。 - mrtsherman
简单而且在大多数情况下有效。 - Aaron

1

还有一种分离用户创建的滚动条的方法:您可以使用替代事件处理程序,例如'mousewheel'、'touchmove'、'keydown'和代码38和40进行箭头滚动,对于使用滚动条滚动-如果'scroll'事件与'mousedown'同时触发直到'mouseup'事件。


可能会有更多的情况导致滚动,例如,在当前屏幕之前尝试在滚动元素中插入一些内容。 - tsh

1

关于:

具体来说,当使用浏览器的后退按钮时,浏览器可能会跳转到上次已知的滚动位置。

这个事件在页面渲染后很快就会触发。您可以将监听滚动事件的延迟设置为1秒左右。


2
这不是一个健壮的解决方案。跳转时间取决于页面加载时间。浏览器会延迟跳转直到页面完全加载。这可以防止加载和改变页面高度的页面元素(如图像)从视图中滚动出去。 - mrtsherman

-1

发现这非常有用。以下是适用于CoffeeScript的版本,供有兴趣的人参考。

$ ->
  S.userScroll = false

  # reset flag on back/forward
  if $.history?
    $.history.init (hash) ->
      S.userScroll = false

  mouseEvent = (e)->
    S.userScroll = true

  $(document).keydown (e) ->
    importantKey =  (e.which == 33 or        # page up
      e.which == 34 or    # page dn
      e.which == 32 or    # spacebar
      e.which == 38 or    # up
      e.which == 40 or    # down
      (e.ctrlKey and e.which == 36) or    # ctrl + home
      (e.ctrlKey and e.which == 35)    # ctrl + end
    )
    if importantKey
      S.userScroll = true;

  # Detect user scroll through mouse
  # Mozilla/Webkit
  if window.addEventListener
      document.addEventListener('DOMMouseScroll', mouseEvent, false);

  # for IE/OPERA etc
  document.onmousewheel = mouseEvent;

-1

感谢您的建议。然而,这是一个二元解决方案 - 检测JavaScript启动的滚动与非JS滚动。我真的需要一个三元解决方案(JavaScript、用户、浏览器)。顺便说一句,jQuery不是您提出的解决方案的要求。 - mrtsherman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接