【问题标题】:Scroll event firing too many times. I only want it to fire a maximum of, say, once per second滚动事件触发太多次。我只希望它每秒最多发射一次
【发布时间】:2012-03-25 16:42:17
【问题描述】:

我有一个带有“无限滚动”的页面。它计算页面末尾和当前页面之间的差异,如果差异足够小,则加载更多内容。使用 jQuery 的代码是这样的:

$(window).on('scroll', function() {
    if (window.pageYOffset > loadMoreButton.offsetTop - 1000)
        # load more content via ajax
}

现在,问题是每次滚动时,每次滚动都会触发多次此事件。我希望最多每 x 毫秒触发一次。我该怎么做?

【问题讨论】:

    标签: javascript


    【解决方案1】:

    查看 Underscore.js 库的“节流”方法。

    http://underscorejs.org/#throttle

    它给出的示例正是您要问的 - 限制了您必须处理滚动事件的频率。

    【讨论】:

    • 非常棒的库,但我试图避免任何依赖。
    • 查看源代码以加下划线(有很好的文档记录),然后拉出油门。
    【解决方案2】:

    滚动多次触发是正确的,您应该能够每次获得不同的滚动位置。我认为您需要在第一次进入滚动事件时设置一个计时器,就像您提到的 x 毫秒,并记录时间戳,然后在下次滚动事件触发时,检查最后一次触发时间,如果它在 x 毫秒内则忽略它,并在您的 Timer 操作中完成真正的工作。

    【讨论】:

      【解决方案3】:
      var isWorking = 0;
      
      $(window).on('scroll', function()
      {
          if(isWorking==0)  
          {
               isWorking=1;
               if (window.pageYOffset > loadMoreButton.offsetTop - 1000)
               # load more content via ajax
               setTimeout(function(){isWorking=0},1000);
          }
      }
      

      【讨论】:

      • 我希望它工作正常,没有测试它。它基本上启动每个滚动,但如果另一个副本正在运行它不会做任何事情,如果不是它等待 xxx 毫秒并完成工作
      • Eeeeck。您不能在 javascript 中使用这样的睡眠功能。它会在睡眠期间挂起浏览器。 Javascript 是单线程的并且会阻塞浏览器。你就是不能写这样的javascript代码。
      • sleep 替换为setTimeout(function(){isWorking=0},1000),它应该可以工作。
      • 这可能会错过最后的滚动事件,因为在滚动停止后,没有触发超时来处理最后的滚动事件。
      【解决方案4】:

      解决此问题的一种方法是定义一个时间间隔,并且在该时间间隔内只处理一次滚动事件。如果在该时间间隔内出现多个滚动事件,则忽略它并仅在该时间间隔过去后处理它。

      var scrollTimer, lastScrollFireTime = 0;
      
      $(window).on('scroll', function() {
      
          var minScrollTime = 100;
          var now = new Date().getTime();
      
          function processScroll() {
              console.log(new Date().getTime().toString());
          }
      
          if (!scrollTimer) {
              if (now - lastScrollFireTime > (3 * minScrollTime)) {
                  processScroll();   // fire immediately on first scroll
                  lastScrollFireTime = now;
              }
              scrollTimer = setTimeout(function() {
                  scrollTimer = null;
                  lastScrollFireTime = new Date().getTime();
                  processScroll();
              }, minScrollTime);
          }
      });
      

      这将立即触发第一个滚动事件,然后在滚动条移动时大约每 100 毫秒为您获取一次滚动事件,然后在滚动条停止移动后触发一个最终事件。您可以通过将参数更改为 setTimeout(当前设置为 100)来调整事件的频率。

      这里有一个演示:http://jsfiddle.net/jfriend00/EBEqZ/,您需要打开一个调试控制台窗口,开始在内容窗口中移动滚动条,然后在调试控制台窗口中观察每个滚动事件的时间。在我的 Chrome 版本中,它们的最小间隔设置为 100 毫秒,并且它们似乎每 100-200 毫秒发生一次。

      【讨论】:

      • 嗨,朋友,当我在无限页面上按键盘的“结束”键时,processScroll 似乎触发了两次。我该如何解决?
      • @user2356198 - 这是解决该问题的版本:jsfiddle.net/v8e7aLy6
      • “结束”键没问题,但是当我快速向下滚动时,它会触发两次。慢速滚动没有问题。
      • @user2356198 - 浏览器无法仅获取结束滚动事件。这种能力根本不存在。它会触发各种中间滚动事件,并且不会告诉您何时完成移动。所以,我们想出了一些变通方法,使用计时器来尝试猜测什么时候没有更多的滚动事件发生,所以这一定是结束。我建议您在 processScroll() 方法中输出 console.log($(window).scrollTop());。该修改旨在确保您不会在同一滚动位置连续两次调用processScroll()
      【解决方案5】:
      var now = new Date().getTime();
      $(window).scroll( function () {
          if (window.pageYOffset > loadMoreButton.offsetTop - 1000)
          {
              if (new Date().getTime() - now > 1000)
              {
                  console.log("Task executed once per second");
                  now = new Date().getTime();
              }
          }
      });
      

      或者

      您可以使用 Throttling 函数调用: throttling-function-calls

      function throttle(fn, threshhold, scope) {
        threshhold || (threshhold = 250);
        var last,
            deferTimer;
        return function () {
          var context = scope || this;
      
          var now = +new Date,
              args = arguments;
          if (last && now < last + threshhold) {
            // hold on to it
            clearTimeout(deferTimer);
            deferTimer = setTimeout(function () {
              last = now;
              fn.apply(context, args);
            }, threshhold);
          } else {
            last = now;
            fn.apply(context, args);
          }
        };
      }
      

      你可以这样称呼它:

      $('body').on('mousemove', throttle(function (event) {
        console.log('tick');
      }, 1000));
      

      【讨论】:

      • 请在此代码中添加一些 cmets 和描述。
      【解决方案6】:

      这是一个不需要使用额外的 JS 库或插件的解决方案,旨在简化。它可能不如其他实现高效,但它绝对比每次滚动时触发主事件更上一层楼。

      本文摘自 Danny Van Kooten 的 blog post。我用它来延迟我博客上的返回顶部按钮的onscroll() 事件。

      var timer;
      $(window).scroll(function() {
          if(timer) {
              window.clearTimeout(timer);
          }
          timer = window.setTimeout(function() {
             // actual code here. Your call back function.
          console.log( "Firing!" );
          }, 100);
      });
      

      您还可以通过将变量移出回调函数来进一步提高性能,以避免不必要的重新计算,例如 $(window).height() 的值或某些静态 div 元素的高度,这些元素在页面加载后不会改变。

      这是一个改编自我的用例的示例。

      var scrollHeight = $("#main-element").height(); //never changes, no need to recalculate.
      $(window).on('scroll', function() {
          if (timer) 
              window.clearTimeout(timer);
          timer = window.setTimeout(function() {
              var scrollPosition = $(window).height() + $(window).scrollTop();    
              if ($(window).scrollTop() < 500)
                  $(".toggle").fadeIn(800);
              else 
                  $(".toggle").fadeOut(800);
          }, 150); //only fire every 150 ms.
      });
      

      这将实际函数限制为仅每 150 毫秒执行一次,否则如果未经过 150 毫秒,则将计时器重置为 0。调整值以适应您的需要。

      【讨论】:

        【解决方案7】:

        jQuery 的创建者 John Resig 给出了一个很酷的解释来解决这种情况。

        var outerPane = $details.find(".details-pane-outer"),
            didScroll = false;
        
        $(window).scroll(function() {
            didScroll = true;
        });
        
        setInterval(function() {
            if ( didScroll ) {
                didScroll = false;
                // Check your page position and then
                // Load in more results
            }
        }, 250);
        

        来源: http://ejohn.org/blog/learning-from-twitter/

        【讨论】:

          【解决方案8】:

          一个像样的油门功能不需要大量的局部变量。节流功能的目的是减少浏览器资源,而不是应用太多的开销,以至于您使用得更多。作为这种说法的证据,我设计了一个节流函数,它的范围内只有 5 个“悬挂”变量引用。此外,我对油门功能的不同用途需要许多不同的环境。这是我认为“好的”节流功能需要的东西清单。

          • 如果自上次调用以来已超过 interval MS,则立即调用该函数。
          • 避免为另一个 interval MS 执行函数。
          • 延迟过多的事件触发,而不是完全放弃事件。
          • 在连续调用时更新延迟的事件对象,使其不会变得“陈旧”。

          而且,我相信下面的节流功能可以满足所有这些要求。

          function throttle(func, alternateFunc, minimumInterval) {
              var executeImmediately = true, freshEvt = null;
              return function(Evt) {
                  if (executeImmediately) { // Execute immediatly
                      executeImmediately = false;
                      setTimeout(function(f){ // handle further calls
                          executeImmediately = true;
                          if (freshEvt !== null) func( freshEvt );
                          freshEvt = null;
                      }, minimumInterval);
                      return func( Evt );
                  } else { // Delayed execute
                      freshEvt = Evt;
                      if (typeof alternateFunc === "function") alternateFunc( Evt );
                  }
              };
          }
          

          然后,将这个节流函数包裹在 DOM 事件侦听器周围:

          var ltCache = [];
          function listen(obj, evt, func, _opts){
              var i = 0, Len = ltCache.length, lF = null, options = _opts || {};
              a: {
                  for (; i < Len; i += 4)
                      if (ltCache[i] === func &&
                        ltCache[i+1] === (options.alternate||null) &&
                        ltCache[i+2] === (options.interval||200)
                      ) break a;
                  lF = throttle(func, options.alternate||null, options.interval||200);
                  ltCache.push(func, options.alternate||null, options.interval||200, lF);
              }
              obj.addEventListener(evt, lF || ltCache[i+3], _opts);
          };
          function mute(obj, evt, func, options){
              for (var i = 0, Len = ltCache.length; i < Len; i += 4)
                  if (ltCache[i] === func &&
                    ltCache[i+1] === (options.alternate||null) &&
                    ltCache[i+2] === (options.interval||200)
                  ) return obj.removeEventListener(evt, ltCache[i+3], options);
          }
          

          示例用法:

          function throttle(func, alternateFunc, minimumInterval) {
              var executeImmediately = true, freshEvt = null;
              function handleFurtherCalls(f){
                  executeImmediately = true;
                  if (freshEvt !== null) func( freshEvt );
                  freshEvt = null;
              };
              return function(Evt) {
                  if (executeImmediately) { // Execute immediatly
                      executeImmediately = false;
                      setTimeout(handleFurtherCalls, minimumInterval);
                      return func( Evt );
                  } else { // Delayed execute
                      freshEvt = Evt;
                      if (typeof alternateFunc === "function") alternateFunc( Evt );
                  }
              };
          }
          var ltCache = [];
          function listen(obj, evt, func, _opts){
              var i = 0, Len = ltCache.length, lF = null, options = _opts || {};
              a: {
                  for (; i < Len; i += 4)
                      if (ltCache[i] === func &&
                        ltCache[i+1] === (options.alternate||null) &&
                        ltCache[i+2] === (options.interval||200)
                      ) break a;
                  lF = throttle(func, options.alternate||null, options.interval||200);
                  ltCache.push(func, options.alternate||null, options.interval||200, lF);
              }
              obj.addEventListener(evt, lF || ltCache[i+3], _opts);
          };
          function mute(obj, evt, func, options){
              for (var i = 0, Len = ltCache.length; i < Len; i += 4)
                  if (ltCache[i] === func &&
                    ltCache[i+1] === (options.alternate||null) &&
                    ltCache[i+2] === (options.interval||200)
                  ) return obj.removeEventListener(evt, ltCache[i+3], options);
          }
          var numScrolls = 0, counter = document.getElementById("count");
          listen(window, 'scroll', function whenbodyscrolls(){
              var scroll = -document.documentElement.getBoundingClientRect().top;
              counter.textContent = numScrolls++;
              if (scroll > 900) {
                console.log('Body scrolling stoped!');
                mute(window, 'scroll', whenbodyscrolls, true);
              }
          }, true);
          <center><h3>\/ Scroll Down The Page \/</h3></center>
          <div style="position:fixed;top:42px"># Throttled Scrolls: <span id="count">0</span></div>
          <div style="height:192em;background:radial-gradient(circle at 6em -5em, transparent 0px, rgba(128,0,0,.4) 90em),radial-gradient(circle at 10em 40em, rgba(255,255,255,.8) 0px, rgba(128,0,0,.02) 50em),radial-gradient(circle at 4em 80em, rgba(0,192,0,.75) 0px,rgba(0,128,0,.56) 10em,rgba(255,0,96,.03125) 30em),radial-gradient(circle at 86em 24em, rgba(255,0,0,.125) 0px,rgba(0,0,255,.0625) 60em,transparent 80em);"></div>
          <style>body{margin:0}</style>

          默认情况下,这会将函数限制为每 200 毫秒最多调用一次。要将间隔更改为不同的毫秒数,请在 options 参数中传递一个名为“interval”的键并将其设置为所需的毫秒数。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2018-08-09
            • 1970-01-01
            • 2013-09-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多