Reputation: 57719
I've have encountered a small annoyance that blows up to be a huge problem.
Problem 1: In Internet Explorer when you close a window (that you opened via window.open
) the ownerDocument
will disappear along with it.
The implication of this is that any call to the DOM, such as appendChild
or createElement
, will fail with a SCRIPT70: Permission Denied
or SCRIPT1717: The interface is unknown
.
I've looked at the behaviour of other browsers such as Chrome. In Chrome ownerDocument
still references the #document
but ownerDocument.defaultView
will eventually be undefined
. This makes sense to me. Calls to appendChild
and createElement
will pass. I think everything is fine so long as you don't try to reference the defaultView
directly.
Problem 2: In Internet Explorer when you click on the close button of the spawned window it doesn't seem to respect the Event loop. I attached an unload
event to the spawned window and it fires immediately instead of queuing it at the end of the Event loop. This does not make sense to me. It becomes quite impossible to deal with this rather trivial problem.
If we just had problem 1 there would be a -still painful but- straightforward solution: check if the ownerDocument
exists and skip if it doesn't. As it is ownerDocument
disappears in the middle of synchronous JavaScript code.
Expected behaviour: a DOM node should not disappear if you've referenced it - garbage collection sanity.
Expected behaviour 2: a DOM node should not disappear in synchronous code. (unless you delete it of course).
Known workaround: move all the code that interacts with the DOM into the window, so that when the window is closed so is the JavaScript runtime environment. This is not a trivial solution and may require significant changes in your architecture.
Crappy solution: wrap any function that does interaction with the DOM in a function that will consume errors if it detects the window of the element has been closed. This is quite invasive and has a significant impact on performance, and IE is already so slow.
Is there a better solution?
What I want, at the very least, is a way to ignore any Error
s that are thrown because the user closed a window. problem 1 and problem 2 break basic assumptions you make about JavaScript code: garbage collection and event loop.
Demo script
<script type="text/javascript">
function go() {
var popup = window.open('', 'open', 'width=500,height=300,scrollbars=yes,resizable=yes');
popup.document.open();
popup.document.write('<html><head></head><body></body></html>');
popup.document.close();
for (var i = 0; i < 10000; i += 1) {
var node = popup.document.createTextNode(i + " ");
popup.document.body.appendChild(node);
}
}
</script>
<input type="button" onclick="go();" value="Open popup" />
(save as .html file)
Instructions:
Here it is a JSFiddle: http://jsfiddle.net/C9p2R/1/
Upvotes: 16
Views: 2303
Reputation: 26647
OK, let me try to simplify. To gain responsiveness of windows and avoid breaking of application when child is closed, several things have to be combined.
setTimeout
or
setInterval
, to schedule DOM-modifying functions, delegate
execution to the child and return immediately. This step makes auto-cleanup possible!setTimeout
or setInterval
, so that events-loop is not locked
for a long time. This gives responsiveness to both windows.Simplest example of child javascript. Function called from parent is LongLastingOperation
and it has been split in 4 chunks:
<script>
function wasteTime(message) {
// TODO: waste time
}
function FirstPartOfLongOperation(){
wasteTime('long... part 1');
setTimeout(SecondPartOfLongOperation, 0);
}
function SecondPartOfLongOperation() {
wasteTime('long... part 2');
setTimeout(ThirdPartOfLongOperation, 0);
}
function ThirdPartOfLongOperation() {
wasteTime('long... part 3');
setTimeout(FourthPartOfLongOperation, 0);
}
function FourthPartOfLongOperation() {
wasteTime('long... part 4');
alert('Done!');
}
// Main entry, called from parent.
// Ex: popup.LongLastingOperation({ data: ....})
function LongLastingOperation(parametersPassedFromParentWindow) {
// decompose long operation
setTimeout(FirstPartOfLongOperation, 0);
}
</script>
Upvotes: 0
Reputation: 26647
After trying couple things (postMessage, Worker,... ) always having problems with IE9, I found nice solution. I made Parallel.For loop using setInterval function. Catch is here:
The setInterval() method will continue calling the function until clearInterval() is called, or the window is closed.
Logic for creating nodes is in the child window, but it's triggered from parent window. Loop is executed in chunks and, because setInterval is used, closing of child window at any point doesn't produce errors. More, browser do not hang (neither parent or child while this is running). And it looks like this:
We have 3 components: parent-ie.html, child-ie.html and small parallel.js file. One quirk was, for all browser to work setInterval(function, -1) was used. Positive values were braking IE, 2nd omitted parameter confuses Opera so it makes only 1st chunk of function. Anyhow, code is here:
parent-id.html
<!DOCTYPE html>
<html>
<head>
<title>Parent</title>
</head>
<body>
<script type="text/javascript">
'use strict';
(function(){
var popup;
var pathname = 'child-ie.html';
window.openPopup = function _open() {
popup = window.open(pathname, 'open', 'width=500,height=300,scrollbars=yes,resizable=yes');
}
window.createElements = function _createElements() {
if (popup == null || popup.closed == true) {
alert("Open new popup window first.");
return;
}
var numberOfElements = parseInt(document.getElementById('numberOfElements').value);
popup.writeElements(numberOfElements);
}
})();
</script>
<button onclick="openPopup()">Open popup</button><br />
<button onclick="createElements()">Create elements</button>
<input id="numberOfElements" type="number" value="10000" />
</body>
</html>
child-ie.html
<!doctype html>
<html>
<head>
<title>Child window</title>
</head>
<body>
<script src="/Scripts/Parallel.js"></script>
<script>
(function(){
function _iterator(index) {
var node = document.createTextNode(index + " ");
document.body.appendChild(node);
}
window.writeElements = function (numberOfElements) {
document.body.innerHTML = '';
Parallel.For(0, numberOfElements, 100, _iterator);
}
})();
</script>
</body>
</html>
/Scripts/Parallel.js
'use strict';
var Parallel;
(function (Parallel) {
var Iterator = (function () {
function Iterator(from, to, step, expression) {
this._from = from;
this._to = to;
this._step = step;
this._expression = expression;
}
Object.defineProperty(Iterator.prototype, "intervalHandle", {
set: function (value) {
this._intervalHandle = value;
},
enumerable: true,
configurable: true
});
Iterator.prototype.next = function () {
var max = this._to > this._step + this._from ? this._step + this._from : this._to;
for(var i = this._from; i < max; i += 1) {
this._expression(i);
}
if(max === this._to) {
clearInterval(this._intervalHandle);
} else {
this._from = max;
}
};
return Iterator;
})();
function For(from, to, step, expression) {
var _iterator = new Iterator(from, to, step, expression);
_iterator.intervalHandle = setInterval(function () {
_iterator.next();
}, -1);
}
Parallel.For = For;
})(Parallel || (Parallel = {}));
Javascript was generated from typescript file (maybe bit more clear then javascript):
'use strict';
module Parallel {
class Iterator {
private _from: number;
private _to: number;
private _step: number;
private _expression: (index: number) => {};
private _intervalHandle: number;
public set intervalHandle(value: number) {
this._intervalHandle = value;
}
constructor(from: number, to: number, step: number, expression: (index: number) => {}) {
this._from = from;
this._to = to;
this._step = step;
this._expression = expression;
}
next() {
var max: number = this._to > this._step + this._from ? this._step + this._from : this._to;
for (var i = this._from; i < max; i += 1) {
this._expression(i);
}
if (max === this._to) {
clearInterval(this._intervalHandle);
}
else {
this._from = max;
}
}
}
export function For(from: number, to: number, step: number, expression: (index: number) => {}) {
var _iterator = new Iterator(from, to, step, expression);
_iterator.intervalHandle = setInterval(function () {
_iterator.next();
}, -1);
}
}
That's all.
Upvotes: 0
Reputation: 57719
Unless anyone has a better solution, I'll go with the crappy solution. Here is my code:
function apply_window_close_fix(dom_element, wrapped_element) {
var ignore_errors = false;
dom_element.ownerDocument.defaultView.addEventListener("unload", function () {
ignore_errors = true;
});
return map(wrapped_element, function (key, func) {
return function () {
try {
return func.apply(this, arguments);
} catch (e) {
if (ignore_errors === false) {
throw e;
}
}
};
});
}
wrapped_element
is the API that I return for modifying the DOM. I've wrapped all functions in a try-catch that will ignore errors if it sees the window has been closed. I call this function only for browsers that behave like Internet Explorer does.
There seems to be only a very minor performance hit. Of course this depends on how intensively you call this API.
One minor downside is that currently rethrowing some Errors is broken in some browsers. Rethrowing a DOMException resets the stack in Internet Explorer and Chrome (and possibly others). I've also found no way to fetch a filename and linenumber from a DOMException in Internet Explorer. Once again, a gross oversight that just ends up wasting time for everybody.
Upvotes: 1
Reputation: 355
You could modify your "Crappy Solution". Instead of wrapping the functions that interact with DOM, you can redefine them when the unload
event is fired in such a way that they don't interact with the DOM, e.g. myOldFunction = function (){};
.
Upvotes: 0