5

Why does another scroll event get called after a scrollTop animation fires its complete callback?

Click Handler:

var lock = false;

$('#id').click(function(event) {
    var pos;
    if (lock) {
        return;
    }
    lock = true;
    pos = 150;

    console.log("jump start");

    $(jQuery.browser.webkit ? "body": "html").animate({ scrollTop: pos }, 150, function () {
        lock = false;
        console.log("jump end");
    });
});

Scroll Handler:

$(window).scroll(function (e) {
    console.log("scrolling");

    if (!lock){
        alert('1');
    }
});

Log:

jump start
scrolling
jump end
scrolling

demo on jsfiddle

xpy
  • 5,481
  • 3
  • 29
  • 48
vlukham
  • 409
  • 4
  • 11
  • Which browser are you checking this in ? – Pat Dobson Oct 18 '13 at 09:02
  • also it could be on first click or on 10, but from 2-9 everything is ok – vlukham Oct 18 '13 at 09:04
  • @majatu, I hope you don't mind but I rephrased your question and updated your code example. If you do mind, please rollback the question. – zzzzBov Feb 19 '14 at 17:32
  • Additionally, [I created my own demo of this issue when I ran into it myself](http://jsfiddle.net/LMR4d/). It appears to happen in chrome, firefox and IE on windows 7, so I imagine it's a consistent behavior. – zzzzBov Feb 19 '14 at 17:34

2 Answers2

11

Background

jQuery scrollTop() uses scrollTo() which is a fire and forget event. There is no stopped event for scrolling. The scroll events occur out of band from scrollTo. scrollTo means 'start scroll', a scroll event means 'scrolled (some position)'. scrollTo just initiates the starting of the scroll, it doesn't guarantee that scrolling finished when it returns. So, jQuery animation completes before final scroll (there could even be multiple scrolls backed up). The alternative would be for jQuery to wait for the position of the scroll to be what it requested (per my soln), but it does not do this

It would be nice if there was a specification that we could point to describing this, but it is just one of the Level 0 dom elements without a spec, see here. I think it makes sense the way that it works, which is why all browsers seem to implement it this way.

Why is this happening

The following occurs on the last scroll of the animation:

  1. jquery: 'Window please scroll this last bit'
  2. Window: 'I got this message from jquery to scroll I will start that now'
  3. jquery: 'woohoo I am finished the animation, I will complete'
  4. Your code: lock = false;console.log("jump end");
  5. Window: 'I have scrolled' call scroll event handlers.'
  6. Your code: $(window).scroll(function (e) { 'Why is this happening?'

As you can see jquery does not wait for the final scroll step of the animation to complete before completing the animation (going on to step 4). Partly this is because there is no stopped event for scrolling and partly this is because jquery does not wait for the scroll position to reach the position that was requested. We can detect when we have reached the destination position as described below.

Solutions

There is no stopped event for when scrolling completes. See here. It makes sense that there is no stopped event because the user could start scrolling again at any point, so there is no point where scrolling has really stopped - the user might just have paused for a fraction of a second.

User scrolling: For user scrolling, the normal approach is to wait some amount of time to see if scrolling is complete as described in the answer of the referenced question (bearing in mind that the user could start scrolling again).

scrollTop: However, since we know the position that we are scrolling to we can do better.

See this fiddle.

The crux of it is that since we know where we are scrolling to, we can store that position. When we reach that position we know that we are done.

The output is now:

jump start
scroll animation
jump end

The code is (note that this is based off your fiddle rather than the code in the edited question):

var scrollingTo = 0;
$('#id').click(function(event) {    
    if (scrollingTo) {
        return;
    }        
    console.log("jump start");
    scrollingTo = 150;
    $(jQuery.browser.webkit ? "body": "html").animate({ scrollTop: scrollingTo }, 150, function () {                
    });
});

function handleScroll()
{    
    if( scrollingTo !== 0  && $(window).scrollTop() == scrollingTo)
    {
        scrollingTo = 0;
        console.log("jump end");    
    }
}

$(window).scroll(function (e) {    
    if (!scrollingTo){
        console.log('user scroll');
    } else {
        console.log("scroll animation");
    }
    handleScroll();
});
Community
  • 1
  • 1
acarlon
  • 16,764
  • 7
  • 75
  • 94
  • What doesn't make sense to me is that the window appears to be triggering the scroll event after an indeterminate amount of time *after* the animation has ended. It would make sense if the scroll event was queued in the event loop to execute after the scroll animation has released. [In my demo, where the completion callback is deferred via a `promise`, the scroll event still occurs after the completion callback](http://jsfiddle.net/LMR4d/). If the scroll event is triggered from changes during the animation, what's causing the large delay between the change to `scrollTop` and the event firing? – zzzzBov Feb 25 '14 at 18:14
  • I think I figured out where the delay comes from. [If you run this fiddle multiple times, you'll see that the scroll callback doesn't always occur before the timeout](http://jsfiddle.net/jSBn2/). That behavior seems inconsistent to me with how other features of JS work, but it explains the significant delay between the animation and the scroll callback. – zzzzBov Feb 25 '14 at 18:30
  • @zzzBov - yes, see my update to background. I originally posted it in the comments, but it became a bit too long. – acarlon Feb 25 '14 at 21:45
  • I saw the comments and the update, and they're what I was looking for. Thanks so much for helping me get to the bottom of this. – zzzzBov Feb 25 '14 at 21:46
0

I believe that at the time the animation ends and the callback function is called, the event has not reached the window yet, so it is not re-called, it just hasn't been fired yet.

xpy
  • 5,481
  • 3
  • 29
  • 48
  • I had a similar thought as well, [so I added a short delay](http://jsfiddle.net/LMR4d/1/), which masks the issue *some* of the time (you may need to run in 5-10 times before it fails). If it were simply because the event hadn't finished bubbling, I'd expect any async delay to fix the issue. – zzzzBov Feb 19 '14 at 18:11
  • @zzzzBov Just for the info, I made a [fiddle](http://jsfiddle.net/pavloschris/47gLg/4/) to check the delay between the animation end and the event fire and it looks like it varies from 4 to 11 ms. On my PC at least. – xpy Feb 19 '14 at 18:25