jQuery中处理$(window).scroll函数的更高效方法?

34
在下面的代码中,我正在检查窗口是否滚动过了特定点,如果滚动超过该点,则更改元素使用固定位置,以便它不会从页面顶部滚动出去。唯一的问题是,它似乎非常消耗客户端内存(并且实际上拖慢了滚动速度),因为在每个滚动像素上,我都在不断更新元素的样式属性。

在尝试更新之前检查属性是否已存在,是否会产生显着差异?是否有完全不同且更有效的方法可以获得相同的结果?

$(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在这个页面右侧使用了黄色的“类似问题”和“帮助”菜单,它们使用了与我所描述的相同的功能。 我想知道他们是如何实现的。

5个回答

60

你可以采用一种技巧,在滚动事件上设置一个计时器,仅在滚动位置短时间内未改变时执行主要操作。我在具有相同问题的调整大小事件上使用了该技术。你可以尝试不同的超时值,以确定何时运行最合适。较短的时间更新会产生更短的滚动暂停,因此可能在滚动期间更频繁地运行,较长的时间需要用户实际暂停所有动作才有意义。你需要尝试确定何时运行最合适,最好在相对较慢的计算机上进行测试,因为在这里滚动延迟的问题最为明显。

这是一个实现的一般思路:

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

1
@Hengjie - 当计时器触发时不重置scrollTimer可能会导致您稍后调用clearTimeout()在一个不再活动的计时器对象上。您不应该这样做,因此更正确的方法是将其设置为null,以便我们知道计时器不再处于活动状态,并且将来不会对其调用clearTimeout()。可能浏览器正在保护免受对clearTimeout()的错误调用,但我喜欢编写正确的代码。 - jfriend00
1
@FredStevens-Smith - 只有两个选项。你要么在用户滚动时不断调用滚动事件并进行大量绘图处理,这会导致非常卡顿的体验;要么设置一个时间延迟,无论多小,只要用户暂停滚动,就会重新绘制。这是一种经典的设计模式,被许多应用程序使用。如果你想要大量重绘,可以将延迟设置得尽可能短,因为它是函数的一个参数。 - jfriend00
1
@FredStevens-Smith - 看看OP的问题 - 由于处理了太多的滚动事件,它变得非常卡顿。在浏览器中解决这个问题的唯一方法是使用小的时间延迟来跳过处理某些事件。也许您认为默认的时间延迟太长了 - 您可以将其缩短。如果您在滚动事件中执行的操作不是计算或绘制密集型的,则可以使用更短的时间延迟。但是,如果浏览器需要一秒钟才能更新屏幕,则无法在每个滚动事件上进行绘制。这是解决此问题的常见方法。 - jfriend00
1
@FredStevens-Smith - 这个解决方案对提问者非常有效。如果你有更好的解决方案,请将其作为答案提出(我们全神贯注地听着)。如果没有,你可能需要重新考虑你的批评和投票,直到有更好的解决方案出现。 - jfriend00
1
@sleepycal - 最合适的时间延迟取决于滚动事件处理程序中正在执行的操作。如果它正在进行非常快速的计算或更新,则50毫秒可能是理想的。如果它正在执行需要浏览器重新布局和重绘1秒钟的操作,则50毫秒太短,会导致卡顿、抖动的体验。这就是为什么延迟是一个参数,你可以根据你的操作设置适当的值。 - jfriend00
显示剩余12条评论

11

我发现对于$(window).scroll(),这种方法更加高效。

var userScrolled = false;

$(window).scroll(function() {
  userScrolled = true;
});

setInterval(function() {
  if (userScrolled) {

    //Do stuff


    userScrolled = false;
  }
}, 50);

查看John Resig的帖子,了解这个主题。

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


2
为什么要持续运行函数,即使滚动没有发生,只是为了检查是否发生了滚动?我更喜欢选定答案的方法。 - Lucky Soni
是的,我同意Lucky的观点。 - Jair Reina
1
选定的答案对我的情况无效,因为选定的答案在滚动仍在进行中时不会触发任何事件。我需要我的事件尽快触发,而不是在滚动结束时才触发。谢谢Sam! - newpxsn
根据哪个更加性能密集,您需要对平均滚动事件的50ms计时器和clearInterval()进行基准测试。 - SleepyCal
@newpxsn - 请看一下我在被采纳的答案下面的评论,这样说通了吗? - Sean Kendle

2
使您的函数更高效一些。在移除/添加样式之前,只需检查style属性是否存在/缺少即可。
$(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");
        }
    }
});

1
以下答案不使用jQuery,但是使用现代浏览器功能实现相同的结果。OP还问是否有不同且更有效的方法。以下方法比jQuery或仅使用onScroll事件监听器的纯JavaScript解决方案要高效得多。
我们将使用两个伟大的东西的组合:
  • position: sticky' CSS属性
  • IntersectionObserver API
为了实现header粘在页面上的效果,我们可以使用position: sticky; top: 0px; CSS属性的惊人组合。这将允许一个元素随着页面滚动并在到达页面顶部时变得粘住(就像它是fixed一样)。
要更改粘住元素或任何其他元素的样式,我们可以使用IntersectionObserver API - 它允许我们观察2个元素的交集中的变化。
为了达到我们想要的效果,我们将添加一个“sentinel”元素,它将在“header”元素到达顶部时作为指示器。换句话说:
- 当sentinel未与视口相交时,我们的“header”位于顶部 - 当sentinel与视口相交时,我们的“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>


1

在这里设置一些逻辑。实际上,您需要在向上和向下时分别设置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;
    }   
});

简单的逻辑,你为我节省了几个小时。 - Mwesigwa

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