Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DOM input events vs. setTimeout/setInterval order

I have a block of JavaScript code running on my page; let's call it func1. It takes several milliseconds to run. While that code is running, the user may click, move the mouse, enter some keyboard input, etc. I have another block of code, func2, that I want to run after all of those queued-up input events have resolved. That is, I want to ensure the order:

  1. func1
  2. All handlers bound to input events that occurred while func1 was running
  3. func2

My question is: Is calling setTimeout func2, 0 at the end of func1 sufficient to guarantee this ordering, across all modern browsers? What if that line came at the beginning of func1—what order should I expect in that case?

Please back up your answers with either references to the relevant specs, or test cases.

Update: It turns out that no, it's not sufficient. What I failed to realize in my original question was that input events aren't even added to the queue until the current code block has been executed. So if I write

// time-consuming loop...
setTimeout func2, 0

then only after that setTimeout is run will any input events (clicks, etc.) that occurred during the time-consuming loop be queued. (To test this, note that if you remove, say, an onclick callback immediately after the time-consuming loop, then clicks that happened during the loop won't trigger that callback.) So func2 is queued first and takes precedence.

Setting a timeout of 1 seemed to work around the issue in Chrome and Safari, but in Firefox, I saw input events resolving after timeouts as high as 80 (!). So a purely time-based approach clearly isn't going to do what I want.

Nor is it sufficient to simply wrap one setTimeout ... 0 inside of another. (I'd hoped that the first timeout would fire after the input events queued, and the second would fire after they resolved. No such luck.) Nor did adding a third, or a fourth, level of nesting suffice (see Update 2 below).

So if anyone has a way of achieving what I described (other than setting a timeout of 90+ milliseconds), I'd be very grateful. Or is this simply impossible with the current JavaScript event model?

Here's my latest JSFiddle testbed: http://jsfiddle.net/EJNSu/7/

Update 2: A partial workaround is to nest func2 inside of two timeouts, removing all input event handlers in the first timeout. However, this has the unfortunate side effect of causing some—or even all—input events that occurred during func1 to fail to resolve. (Head to http://jsfiddle.net/EJNSu/10/ and try rapidly clicking the link several times to observe this behavior. How many clicks does the alert tell you that you had?) So this, again, surprises me; I wouldn't think that calling setTimeout func2, 0, where func2 sets onclick to null, could prevent that callback from being run in response to a click that happened a full second ago. I want to ensure that all input events fire, but that my function fires after them.

Update 3: I posted my answer below after playing with this testbed, which is illuminating: http://jsfiddle.net/TrevorBurnham/uJxQB/

Move the mouse over the box (triggering a 1-second blocking loop), then click multiple times. After the loop, all the clicks you performed play out: The top box's click handler flips it under the other box, which then receives the next click, and so on. The timeout triggered in the mouseenter callback does not consistently occur after the click events, and the time it takes for the click events to occur varies wildly across browsers even on the same hardware and OS. (Another odd thing this experiment turned up: I sometimes get multiple jQuery mouseenter events even when I move the mouse steadily into the box. Not sure what's going on there.)

like image 981
Trevor Burnham Avatar asked Jun 17 '11 20:06

Trevor Burnham


People also ask

What is the precedence in event loop?

The event loop is actually composed of one or more event queues. In each queue, events are handled in a FIFO order. It's up to the browser to decide how many queues to have and what form of prioritisation to give them. There's no Javascript interface to individual event queues or to send events to a particular queue.

What is difference between setInterval and setTimeout?

setTimeout allows us to run a function once after the interval of time. setInterval allows us to run a function repeatedly, starting after the interval of time, then repeating continuously at that interval.

How does nested setTimeout work?

The nested setTimeout method is more flexible than setInterval . For example, you want to write a service for sending a request to the server once in 5 seconds to ask for data. If the server is busy, the interval will be increased to 10, 20, 40 seconds, and more.

Is setTimeout blocking?

Explanation: setTimeout() is non-blocking which means it will run when the statements outside of it have executed and then after one second it will execute. All other statements that are not part of setTimeout() are blocking which means no other statement will execute before the current statement finishes.


2 Answers

I think you are on the wrong track with your experiments. One problem is of course that you are fighting different message loop implementations here. The other (the one you didn't recognize it seems) is different double click handling. If you click the link twice you won't get two click events in MSIE - it's rather one click event and a dblclick event (for you that looks like the second click was "swallowed"). All other browsers seem to generate two click events and a dblclick event in this scenario. So you need to handle dblclick events as well.

As message loops go, Firefox should be easiest to handle. From all I know, Firefox adds messages to the queue even when JavaScript code is running. So a simple setTimeout(..., 0) is sufficient to run code after the messages are processed. You should refrain from hiding the link after func1() is done however - at this point clicks aren't processed yet and they won't trigger event handlers on a hidden element. Note that even a zero timeout doesn't get added to the queue immediately, current Firefox versions have 4 milliseconds as the lowest possible timeout value.

MSIE is similar, only that there you need to handle dblclick events as I mentioned before. Opera seems to work like that as well but it doesn't like it if you don't call event.preventDefault() (or return false from the event handler which is essentially the same thing).

Chrome however seems to add the timeout to the queue first and only add incoming messages after that. Nesting two timeouts (with zero timeout value) seems to do the job here.

The only browser where I cannot make things work reliably is Safari (version 4.0 on Windows). The scheduling of messages seems random there, looks like timers there execute on a different thread and can push messages into the message queue at random times. In the end you probably have to accept that your code might not get interrupted on the first occasion and the user might have to wait a second longer.

Here is my adaptation of your code: http://jsfiddle.net/KBFqn/7/

like image 97
Wladimir Palant Avatar answered Sep 17 '22 14:09

Wladimir Palant


If I'm understanding your question correctly, you have a long-running function but you don't want to block the UI while it is running? After the long-running function is done you then want to run another function?

If so instead of using timeouts or intervals you might want to use Web Workers instead. All modern browsers including IE9 should support Web Workers.

I threw together an example page (couldn't put it on jsfiddle since Web Workers rely on an external .js file that has to be hosted on the same origin).

If you click A, B, C or D a message will be logged on the right. When you press start a Web Worker starts processing for 3 seconds. Any clicks during those 3 seconds will be immediately logged.

The important parts of the code are here:

func1.js The code that runs inside the Web Worker

onmessage = function (e) {
    var result,
    data = e.data, // get the data passed in when this worker was called
                   // data now contains the JS literal {theData: 'to be processed by func1'}
    startTime;
    // wait for a second
    startTime = (new Date).getTime();
    while ((new Date).getTime() - startTime < 1000) {
        continue;
    }
    result = 42;
    // return our result
    postMessage(result);
}

The code that invokes the Web Worker:

var worker = new Worker("func1.js");
// this is the callback which will fire when "func1.js" is done executing
worker.onmessage = function(event) {
    log('Func1 finished');
    func2();
};

worker.onerror = function(error) {
    throw error;
};

// send some data to be processed
log('Firing Func1');
worker.postMessage({theData: 'to be processed by func1'});
like image 28
Useless Code Avatar answered Sep 18 '22 14:09

Useless Code