DOM input events vs. setTimeout/setInterval order
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/
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'});
You can use dispatchEvent
with a custom event name at the end of your function. This won't work on IE, but is still possible; just use fireEvent
instead.
Take a look at this:
http://jsfiddle.net/minitech/NsY9V/
Click "start the long run", and click on the textbox and type in it. Voilà!
At this point, I'm prepared to say that, regrettably, there is no solution to this problem that will work under all browsers, in every scenario, every time. In a nutshell: If you run a JavaScript function, there's no way to reliably distinguish between input events that the user triggered during that time and those the user triggered afterward. This has interesting implications for JS developers, especially those working with interactive canvases.
My mental model of how JS input events work was off the mark. I'd thought that it went
- The user clicks a DOM element while code is running
- If that element has a
click
event handler, the callback is queued - When all blocking code has executed, the callback is run
However, my experiments, and those contributed by Wladimir Palant (thanks, Wladimir) show that the correct model is
- The user clicks a DOM element while code is running
- The browser captures the coordinates, etc. of the click
- Some time after all blocking code has executed, the browser checks which DOM element is at those coordinates, then runs the callback (if any)
I say "some time after" because different browsers seem to have very different behaviors for this—in Chrome for Mac, I can set a setTimeout func2, 0
at the end of my blocking code and expect func2
to run after the click callbacks (which run only 1-3ms after the blocking code finished); but in Firefox, the timeout always resolves first, and the click callbacks typically happen ~40ms after the blocking code finished executing. This behavior is apparently beyond the purview of any JS or DOM spec. As John Resig put it in his classic How JavaScript Timers Work:
When an asynchronous event occurs (like a mouse click, a timer firing, or an XMLHttpRequest completing) it gets queued up to be executed later (how this queueing actually occurs surely varies from browser-to-browser, so consider this to be a simplification).
(Emphasis mine.)
So what does this mean from a practical standpoint? This is a non-issue as the execution time of blocking code approaches 0. Which means that this problem is yet another reason to hew to that old advice: Break up your JS operations into small chunks to avoid blocking the thread.
Web workers, as Useless Code suggested, are even better when you can use them—but be aware that you're foregoing compatibility with Internet Explorer and all major mobile browsers.
Finally, I hope browser-makers will move forward on standardizing input events in the future. This is one of many quirks in that area. I hope Chrome will lead the way to the future: excellent thread isolation, low event latency, and relatively consistent queueing behavior. A web developer can dream, can't he?