【问题标题】:More efficient way to handle $(window).scroll functions in jquery?在 jquery 中处理 $(window).scroll 函数的更有效方法?
【发布时间】:2011-09-12 18:15:00
【问题描述】:

在下面的代码中,我正在检查窗口是否正在滚动超过某个点,如果是,则将元素更改为使用固定位置,以便它不会滚动到页面顶部。唯一的问题是这似乎是高度客户端内存密集型(并且确实会降低滚动速度),因为在每个滚动像素上,我都会一遍又一遍地更新元素的样式属性。

在尝试更新之前检查 attr 是否已经存在会产生重大影响吗?是否有完全不同且更有效的做法来获得相同的结果?

$(window).scroll(function () {
    var headerBottom = 165;
    var fcHeight = $("#pnlMainNavContainer").height();

    var ScrollTop = $(window).scrollTop();
    if (ScrollTop > headerBottom) {
        $("#HeaderContentBuffer").attr("style", "margin-top:" + (fcHeight) + "px;");
        $("#AddFieldsContainer").attr("style", "position:fixed;width:320px;top:70px;left:41px;");
    } else {
        $("#HeaderContentBuffer").attr("style", "margin-top: 0px;");
        $("#AddFieldsContainer").removeAttr("style");
    }
});

在我输入此内容时,我注意到 StackOverflow.com 使用与此页面右侧黄色“类似问题”和“帮助”菜单相同类型的功能。我想知道他们是怎么做到的。

【问题讨论】:

    标签: jquery scroll


    【解决方案1】:

    您可以使用的一种技术是在滚动事件上设置一个计时器,并且仅在滚动位置在短时间内没有改变时才执行主要工作。我在具有相同问题的调整大小事件上使用该技术。您可以试验一下似乎可以正常工作的超时值。更短的时间更新与更短的滚动暂停,因此可能在滚动期间更频繁地运行,更长的时间需要用户实际暂停所有运动有意义的时间。您将不得不试验哪种超时值最适合您的目的,最好在相对较慢的计算机上进行测试,因为这是滚动延迟问题最明显的地方。

    以下是如何实现的总体思路:

    var scrollTimer = null;
    $(window).scroll(function () {
        if (scrollTimer) {
            clearTimeout(scrollTimer);   // clear any previous pending timer
        }
        scrollTimer = setTimeout(handleScroll, 500);   // set new timer
    });
    
    function handleScroll() {
        scrollTimer = null;
        var headerBottom = 165;
        var fcHeight = $("#pnlMainNavContainer").height();
    
        var ScrollTop = $(window).scrollTop();
        if (ScrollTop > headerBottom) {
            $("#HeaderContentBuffer").attr("style", "margin-top:" + (fcHeight) + "px;");
            $("#AddFieldsContainer").attr("style", "position:fixed;width:320px;top:70px;left:41px;");
        } else {
            $("#HeaderContentBuffer").attr("style", "margin-top: 0px;");
            $("#AddFieldsContainer").removeAttr("style");
        }
    }
    

    您还可以通过在第一次滚动开始时缓存一些选择器来加快滚动功能,这样就不必每次都重新计算它们。这是每次创建 jQuery 对象的额外开销可能对您没有帮助的地方。


    这是一个为您处理滚动计时器的 jQuery 附加方法:

    (function($) {
        var uniqueCntr = 0;
        $.fn.scrolled = function (waitTime, fn) {
            if (typeof waitTime === "function") {
                fn = waitTime;
                waitTime = 500;
            }
            var tag = "scrollTimer" + uniqueCntr++;
            this.scroll(function () {
                var self = $(this);
                var timer = self.data(tag);
                if (timer) {
                    clearTimeout(timer);
                }
                timer = setTimeout(function () {
                    self.removeData(tag);
                    fn.call(self[0]);
                }, waitTime);
                self.data(tag, timer);
            });
        }
    })(jQuery);
    

    工作演示:http://jsfiddle.net/jfriend00/KHeZY/

    然后您的代码将像这样实现:

    $(window).scrolled(function() {
        var headerBottom = 165;
        var fcHeight = $("#pnlMainNavContainer").height();
    
        var ScrollTop = $(window).scrollTop();
        if (ScrollTop > headerBottom) {
            $("#HeaderContentBuffer").attr("style", "margin-top:" + (fcHeight) + "px;");
            $("#AddFieldsContainer").attr("style", "position:fixed;width:320px;top:70px;left:41px;");
        } else {
            $("#HeaderContentBuffer").attr("style", "margin-top: 0px;");
            $("#AddFieldsContainer").removeAttr("style");
        }
    });
    

    【讨论】:

    • @Hengjie - 在计时器触发时不重置scrollTimer 可能会导致您稍后在不再活动的计时器对象上调用clearTimeout()。您不应该这样做,因此设置为null 更正确,因此我们知道计时器不再处于活动状态,并且将来不会调用clearTimeout()。可能浏览器正在防止对clearTimeout() 的错误调用,但我喜欢正确编码。
    • @FredStevens-Smith - 只有两个选项。您要么在用户滚动时通过大量更新不断调用滚动事件,如果您在滚动处理程序中进行大量绘图,这会产生非常滞后的体验,或者您设置时间延迟,无论您想要多么小,以便当用户暂停滚动时,然后它会重绘。这是一种经典的设计模式,被大量的应用程序使用。如果您想要大量重绘,您可以将延迟设置为任意短,因为它是函数的参数。
    • @FredStevens-Smith - 看看 OP 的问题 - 因为他们处理了太多滚动事件,所以它变得非常滞后。在浏览器中解决这个问题的唯一方法是用一小段时间延迟来跳过处理某些事件。也许您认为默认时间延迟太长 - 欢迎您将其缩短。如果您在滚动事件中执行的操作不是计算密集型或绘制密集型,则可以用更短的时间延迟逃脱。但是,如果浏览器需要一秒钟来更新屏幕,那么您就不能在每个滚动事件上进行绘制。这是此问题的常见解决方案。
    • @FredStevens-Smith - 该解决方案适用于 OP。如果您有更好的解决方案,请将其作为答案提出(我们都在听)。如果没有,您可能需要重新考虑您的批评和反对意见,直到找到更好的解决方案。
    • @sleepycal - 最合适的时间延迟实际上取决于滚动事件处理程序中正在执行的操作。如果它正在进行非常快速的计算或更新,那么 50ms 可能是理想的。如果它正在做的事情需要浏览器 1 秒来重新布局和重绘,那么 50 毫秒太短了,并且会产生延迟、生涩的体验。这就是为什么延迟是一个参数,您可以根据自己的操作进行设置。
    【解决方案2】:

    我发现这种方法对$(window).scroll() 更有效

    var userScrolled = false;
    
    $(window).scroll(function() {
      userScrolled = true;
    });
    
    setInterval(function() {
      if (userScrolled) {
    
        //Do stuff
    
    
        userScrolled = false;
      }
    }, 50);
    

    查看John Resig's post 了解此主题。

    一个更高效的解决方案是设置一个更长的时间间隔来检测你是否 靠近页面底部或顶部。这样,您甚至不必使用$(window).scroll()

    【讨论】:

    • 为什么即使滚动没有发生也要继续运行函数只是为了检查滚动是否发生?我更喜欢选择的答案方法。
    • 是的,我同意 Lucky 的观点
    • 所选答案不适用于我的情况,因为如果滚动仍在进行中,所选答案不会触发任何事件。我需要我的事件尽快触发,而不是在滚动结束时触发。谢谢山姆!
    • 取决于哪个性能更密集,您必须在滚动事件的平均数量等上对 50ms 计时器和 clearInterval() 进行基准测试。
    • @newpxsn - 在接受的答案下方查看我的评论,这有意义吗?
    【解决方案3】:

    让你的功能更高效。

    只需在删除/添加样式之前检查样式属性是否存在/不存在。

    $(window).scroll(function () {
        var headerBottom = 165;
        var fcHeight = $("#pnlMainNavContainer").height();
    
        var ScrollTop = $(window).scrollTop();
        if (ScrollTop > headerBottom) {
            if (!$("#AddFieldsContainer").attr("style")) {
                $("#HeaderContentBuffer").attr("style", "margin-top:" + (fcHeight) + "px;");
                $("#AddFieldsContainer").attr("style", "position:fixed;width:320px;top:70px;left:41px;");
            }
        } else {
            if ($("#AddFieldsContainer").attr("style")) {
                $("#HeaderContentBuffer").attr("style", "margin-top: 0px;");
                $("#AddFieldsContainer").removeAttr("style");
            }
        }
    });
    

    【讨论】:

      【解决方案4】:

      在这里设置一些逻辑。您实际上需要设置 atts 一次,一次向下。所以:

      var checker = true;
      $(window).scroll(function () {
      
          .......
      
          if (ScrollTop > headerBottom && checker == true) {
              $("#HeaderContentBuffer").attr("style", "margin-top:" + (fcHeight) + "px;");
              $("#AddFieldsContainer").attr("style", "position:fixed;width:320px;top:70px;left:41px;");
              checker == false;
          } else if (ScrollTop < headerBottom && checker == false) {
              $("#HeaderContentBuffer").attr("style", "margin-top: 0px;");
              $("#AddFieldsContainer").removeAttr("style");
              checker == true;
          }   
      });
      

      【讨论】:

      • 简单的逻辑,你为我节省了几个小时。
      【解决方案5】:

      以下答案不使用 jQuery,但使用现代浏览器功能实现了相同的结果。 OP还询问是否有不同且更有效的方法。以下方法比 jQuery 甚至使用 onScroll 事件侦听器的纯 JavaScript 解决方案性能要高得多。

      我们将结合使用 2 个很棒的东西:

      • position: sticky' CSS 属性
      • IntersectionObserver API

      要实现header 卡住的效果,我们可以使用position: sticky; top: 0px; CSS 属性的惊人组合。这将允许一个元素随着页面滚动并在到达页面顶部时卡住(好像它是fixed)。

      要更改卡住元素或任何其他元素的样式,我们可以使用IntersectionObserver API - 它允许我们观察两个元素交集的变化。

      为了达到我们想要的效果,我们将添加一个sentinel 元素,当header 元素到达顶部时,它将作为一个指示器。换句话说:

      • 当哨兵不与视口相交时,我们的header 位于顶部
      • 当哨兵与视口相交时,我们的header不在顶部

      有了这两个条件,我们可以对header或其他元素应用任何必要的样式。

      const sentinelEl = document.getElementById('sentinel')
      const headerEl = document.getElementById('header')
      const stuckClass = "stuck"
      
      const handler = (entries) => {
        if (headerEl) {
          if (!entries[0].isIntersecting) {
            headerEl.classList.add(stuckClass)
          } else {
            headerEl.classList.remove(stuckClass)
          }
        }
      }
      
      const observer = new window.IntersectionObserver(handler)
      observer.observe(sentinelEl)
      html,
      body {
        font-family: Arial;
        padding: 0;
        margin: 0;
      }
      
      .topContent,
      .pageContent,
      #header {
        padding: 10px;
      }
      
      #header {
        position: sticky;
        top: 0px;
        transition: all 0.2s linear;
      }
      
      #header.stuck {
        background-color: red;
        color: white;
        
      }
      
      .topContent {
        height: 50px;
        background-color: gray;
      }
      
      .pageContent {
        height: 600px;
        background-color: lightgray;
      }
      <div class="topContent">
        Content above our sticky element
      </div>
      <div id="sentinel"></div>
      <header id="header">
        Sticky header
      </header>
      <div class="pageContent">
        The rest of the page
      </div>

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-05-09
        • 2015-05-15
        • 2023-03-19
        • 2012-12-02
        相关资源
        最近更新 更多