Problem
In Firefox, if window.open is called before a previous call to this method had a chance to return, multiple windows would appear, even if the same window name is used in all calls. You might ask yourself, when does this really happen? Well, just think of an event handler being fired multiple times. Let this event be an onclick event and lets use the following event handler for it:
<script type="text/javascript" language="JavaScript">
function openPopup() {
// should only open one popup window even after multiple calls... but does it?
window.open("http://www.google.com", "popup", "width=500,height=500");
}
</script>
.
.
<a href="javascript:openPopup();" onclick="openPopup();">example</a>
.
.
Click on the example link in Firefox as fast as you can example. You should see multiple popups opening if you clicked fast enough.
But wait, Mozilla recommends a different way to open a new window. Consider the following code:
function openPopup2() {
var windowObjectReference = null;
if(windowObjectReference == null || windowObjectReference.closed) {
// should only open one popup window even after multiple calls... but does it?
windowObjectReference = window.open("http://www.google.com", "popup", "width=500,height=500");
}
else {
windowObjectReference.focus();
}
}
But this does not work either. The windowObjectReference remains null even after the initial call to window.open. This is due to the fact that the function does not return immediately like the documentation suggests (the function is described as Asynchronous). More about that in the next section.
The bug you have just witnessed is described by Mozilla here. This bug has been open for 5 years now and is not yet fixed in Firefox 4.0 beta (although the speed ups to the JavaScript engine help mask the problem).
In IE 7 multiple windows will open, but once the page has been loaded into the popup window, other windows with the same name will be closed. IE 8 seems to have gotten rid of this problem completely. All other browsers seem to act as expected, so this is almost purely a Firefox issue.
Cause
Although I can only speculate about the true cause of this problem without diving into the code, after a few test I believe I narrowed down the root cause of this problem. The problem seems to be caused by the way that Firefox decided to handle event dispatching in JavaScript. Like in dispatchers in other browsers, the events are entered into a priority queue by time. However, unlike in Opera for example where an event handler is executed to its' very end, here it can be interrupted mid-way through when waiting for I/O. Modern operating systems usually block threads/processes waiting for I/O and let other processes run in the meantime. When I/O is ready to be consumed, the thread/process wakes up. This seems to fit our symptoms perfectly. Opening a new window usually results in a new thread or a new process, and since a request to open a URL in a new window is an I/O request, the thread is blocked. Now a new event can be serviced and it's handler can be invoked, but in our case it is the same event and the same handler. This is exactly what causes the problem, the order events in the queue are serviced generates some unexpected results.
To convince ourselves lets create a little experiment:
<html>
<head>
<title>Popup Window Experiment</title>
<script language="javascript">
// Call count will count the number of interrupted calls, if there are no interruptions
// then it will always be 1 when the function body is executed otherwise it will be more than 1
// meaning that the previous call to the handler did not get to complete it job and decrement the
// variable back to its original value before the call.
var callsCount = 0;
var callsCount2 = 0;
var myWinObj = null;
// this handler will be blocked when waiting for I/O, we will see this by observing
// pop-up windows with numbers 1, 2, 3, 4... matching the number of clicks while waiting for I/O
function blockedHandler() {
callsCount++;
myWinObj = window.open("", "myWin", "width=100,height=100");
myWinObj.document.write(callsCount);
callsCount--;
}
function pause(millis) {
var date = new Date();
var curDate = null;
do {
curDate = new Date();
} while(curDate - date < millis)
}
// this handler will not be blocked, demonstrating that when there is no I/O event handlers
// are not blocked (also known as non-preemptive behaviour)
function nonBlockedHandler() {
callsCount2++;
pause(3000);
document.body.innerHTML += '<br />' + callsCount2;
callsCount2--;
}
</script>
</head>
<body>
<p>
Click on the following link multiple times really fast, to see that you will get different numbers in each new popup window in
Firefox:<br /><a href="javascript:blockedHandler();">Blocked on I/O Popup Demo</a>
</p>
<p>
Note: how the lowest number (1) is on top, indicating the first call finishes last.
</p>
<p>
Click here to see that non-I/O events do not get interrupted during the execuation of their event handler:<br />
<a href="javascript:nonBlockedHandler();">Not-Blocked on non-I/O</a>
</p>
</body>
</html>
This experimental code is very important to understand since we will use it as the basis for our work around. You can try it on this page here.
The blockedHandler function demonstrates how an I/O event -- loading the url into the new window -- is interrupted in the middle, evident by the different numbers printed in the new windows. If the event handler was not interrupted then the count would drop back to the original value when it reaches the _callsCount-- _line. Instead, it enters another event handler and increments the value again. So the printed value will be different than the original. It is also important to note how the first call completes its work last (again we can tell by looking at the numbers).
The nonBlockedHandler function demonstrates how a non-I/O event, or a CPU intensive event, does not get blocked and runs until completion. In contrast to the previous test that gave different numbers in each window, this test produces the same number in the main application window. Since there is no I/O, nothing gets blocked, only a very intensive loop. This shows JavaScript is single threaded, the event handlers themselves do not get preempted. However, as the browser creates new threads for a new window in the blockedHandler these threads do get preempted by the OS when they ask for I/O.
To conclude our findings, JavaScript is indeed single thread, but the browser is using multi-threading which causes our symptoms. So how do we cure concurrency problems just by using our single threaded JavaScript?
Workaround
Initial Design
Not giving up on Firefox, I came up with a simple work around that looks like this:
var linkOnClickCalled = 0;
var linkOnClick = function() {
linkOnClickCalled++;
if(linkOnClickCalled <= 1) {
window.open('newWin', 'width=400,height=400');
}
linkClickCalled--;
}
The idea was simple, use the results of our previous experiment to skip all the calls until our event handler returns. But this looks a little ugly and is not very scalable, therefore I cam up with the next design.
Refined Design
Notice the similarity of the initial design to how Semaphores work. A good place to look for inspiration is then of course the Linux c library semaphore.h, which we will use to improve this work around:
var windowObjectReference = null; // global variable
/**
* The skip semaphore is the structure that allows us to ensure that a function is only
* called initCount number of times before it returns. This only matters if the function
* handles I/O in Firefox. For all other functions they will only be invoked once during
* the function execution (not including recursive calls).
*
* @param int initCount The number of calls allowed during function execution
* @param Function func A function to be invoked
* @return Function The inner function that will do the work of the skipping semaphore.
*/
var skipSemaphore = function(initCount, func) {
var count = initCount;
var sem_wrapper = function() {
count--;
if(count >= 0) {
func();
}
count++;
};
return sem_wrapper;
};
// example of using the skip semaphore to write a function that opens a new window
var openPopup = skipSemaphore(1, function() {
if(windowObjectReference == null || windowObjectReference.closed)
{
windowObjectReference = window.open("http://www.google.com", "myWin", "width=100,height=100");
}
else
{
windowObjectReference.focus();
}
});
The first change I had to make was to invert the count to start at n and go to 0, just like a true semaphore. Next, I used the idea of closures to get rid of the global variable we used for counting. This piece was inspired by Douglas Crockford's, _JavaScriptThe Good Parts, _page 44 which describes a very similar technique to make caching reusable. It works due to JavaScript inner function staying alive even as outer functions return, allowing access to the outer function's variables.
Note that unlike true semaphores that block a thread if the count is smaller or equal to zero (<= 0), our code just skips that section. This is why I called it a skip semaphore here. Also, there is no queue to keep waiting events (since we skipped them). Finally, there is no signal and wait. So in the true spirit of JavaScript we have something not seen anywhere else, but still bares some resembles to other things.
References:
Mozilla Bug Report - https://bugzilla.mozilla.org/show_bug.cgi?id=279670
Opera Developer's Channel, Timing and Synchronization in JavaScript - http://dev.opera.com/articles/view/timing-and-synchronization-in-javascript/
Concurrency in JavaScript - http://endertech.blogspot.com/2010/01/investigating-javascript-concurrency.html
Mutex in JavaScript - http://www.developer.com/lang/jscript/article.php/3592016/AJAX-from-Scratch-Implementing-Mutual-Exclusion-in-JavaScript.htm
Separate Processes For Tabs, Mozilla Wiki - https://wiki.mozilla.org/Content_Processes
Douglas Crockford, _JavaScript: The Good Parts, _2008 Yahoo Press