Search code examples
javascriptscrollsetintervalevent-looponscroll

Why is an interval function executed before an event callback?


I realized something odd about the javascript on scroll event.

Until now I was always convinced that it would be fired directly, whenever the scroll position changes.
And because javascript is blocking, a callback would always be the first thing to be executed and the first function to "see" this new value – or so I thought.

Here I have simple setup, where a global value gets updated to the current scroll position, on scroll. Then there's an interval comparing the two.
Now if you scroll around very fast it does sometimes happen, that the cached value is not the same as the actual returned scroll position.

// cache scroll pos
var scrollPos = 0;
// update on scroll
window.onscroll = function () {
    scrollPos = scrollTop();
};
// compare on interval
setInterval(function () {
    if (scrollPos != scrollTop()) {
        console.log("Out of sync!  cached:", scrollPos, "| actual:", scrollTop());
    }
}, 100);

fiddle: http://jsfiddle.net/71vdx4rv/2/
You can try scrolling around quickly or using the button. [tested on Chrome, Safari, FF, Opera]

Why is the interval function executed and knows about the 'new' scroll position, before the callback is?
Can someone please explain this to me?


Solution

  • Until now I was always convinced that it would be fired directly, whenever the scroll position changes.

    Unfortunately (and evidently) that's not the case. While DOM 3 does state

    A user agent MUST dispatch [the scroll] event when a document view or an element has been scrolled. This event type is dispatched after the scroll has occurred.

    this doesn't mean "immediately". The CSSOM draft clarifies:

    [To perform a scroll, repeatedly run these steps]:

    1. Perform a [smooth or instant] scroll of box to position.
    2. Queue a task to run task, unless a task to run task is in the queue.

    [where task is does fire a scroll event at the respective target]

    "Queuing a task" explicitly means that the event is dispatched asynchronously, so the scroll position might already have been changed when a waiting task (like your timer) is executed before the scroll-event-firing task.

    Also, when performing a smooth scroll, the browser is allowed to change the scroll position multiple times over an arbitrary timespan, before queuing the task to fire the scroll event.