关闭窗口会打破事件循环假设

时间:2013-02-15 13:28:25

标签: javascript internet-explorer internet-explorer-9

我遇到了一个令人烦恼的小烦恼。

问题1:在关闭窗口(通过window.open打开)的Internet Explorer中,ownerDocument将随之消失。

这意味着任何对DOM的调用(例如appendChildcreateElement)都会因SCRIPT70: Permission DeniedSCRIPT1717: The interface is unknown而失败。

我查看了Chrome等其他浏览器的行为。在Chrome中ownerDocument仍然引用#document,但ownerDocument.defaultView最终会undefined。这对我来说很有意义。致appendChildcreateElement的电话将通过。我认为一切都很好,只要你不试图直接引用defaultView

问题2:在Internet Explorer中单击生成窗口的关闭按钮时,它似乎不尊重事件循环。我将unload事件附加到生成的窗口,它立即触发 ,而不是在Event循环结束时对其进行排队。这对对我有意义。处理这个相当微不足道的问题变得十分不可能。

如果我们只是遇到问题1 ,那么会有一个令人痛苦但又直截了当的解决方案:检查ownerDocument是否存在,如果不存在则跳过。{因为ownerDocument在同步JavaScript代码的中间消失了。

预期的行为:如果您引用了DOM节点,它不应该消失 - 垃圾收集整理。

预期的行为2: DOM节点不应该在同步代码中消失。 (除非你当然删除它。)

已知的解决方法:将与DOM交互的所有代码移动到窗口中,以便在窗口关闭时,JavaScript运行时环境也是如此。这不是一个简单的解决方案,可能需要对您的架构进行重大更改。

Crappy解决方案:在一个函数中包装任何与DOM交互的函数,如果它检测到元素的窗口已经关闭,它将消耗错误。这是非常具有侵略性并且对性能有重大影响,IE已经很慢了。

有更好的解决方案吗?

我想要的是,忽略由于用户关闭窗口而引发的任何Error的方法。 问题1 问题2 打破了您对JavaScript代码的基本假设:垃圾收集和事件循环。


演示脚本

<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" />

(另存为.html文件)

说明:

  • 在Internet Explorer 9中打开
  • 点击“打开弹出窗口”
  • 在渲染时关闭窗口
  • 观察“权限被拒绝”

这是一个JSFiddle:http://jsfiddle.net/C9p2R/1/

4 个答案:

答案 0 :(得分:1)

除非有人有更好的解决方案,否则我会选择糟糕的解决方案。这是我的代码:

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是我为修改DOM而返回的API。我已将所有函数包装在try-catch中,如果它看到窗口已关闭,它将忽略错误。我只对那些表现得像Internet Explorer的浏览器调用此函数。

似乎只有非常小的性能影响。当然,这取决于您对此API的称呼程度。

一个小的缺点是,在某些浏览器中,当前重新抛出某些错误会被破坏。重新抛出DOMException会重置Internet Explorer和Chrome(以及可能的其他)中的堆栈。我还发现无法从Internet Explorer中的DOMException获取文件名和亚麻布。再次,一个严重的疏忽,最终会浪费时间给每个人。

答案 1 :(得分:0)

您可以修改“Crappy Solution”。您可以在unload事件以不与DOM交互的方式触发时重新定义它们,而不是包装与DOM交互的函数。 myOldFunction = function (){};

答案 2 :(得分:0)

在尝试了几件事(postMessage,Worker,......)总是遇到IE9问题之后,我找到了很好的解决方案。我使用setInterval函数创建了Parallel.For循环。 Catch在这里:

  

setInterval()方法将继续调用函数直到   调用clearInterval(),或关闭窗口

创建节点的逻辑位于子窗口中,但它是从父窗口触发的。循环以块的形式执行,因为使用了setInterval,所以在任何点关闭子窗口都不会产生错误。此外,浏览器不会挂起(在运行时不会挂起父级或子级)。它看起来像这样:

enter image description here

我们有3个组件:parent-ie.html,child-ie.html和small parallel.js文件。一个怪癖是,所有浏览器都使用setInterval(函数,-1)。正值是制动IE,第二个省略参数混淆Opera所以它只产生第一个功能块。无论如何,代码在这里:

<强>父 - 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>

儿童-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是从typescript文件生成的(可能比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);
    }
}

就是这样。

答案 3 :(得分:0)

好的,让我试着简化一下。为了获得窗口的响应性并避免在孩子关闭时破坏应用程序,必须结合几件事。

  1. 父级不应直接修改子DOM。功能应该在 孩子本身。 但是,如果这样的话,这根本不会解决任何问题 函数从父级触发并同步执行。 挂起的窗户和例外仍在那里。
  2. 父母呼叫孩子的主要功能必须使用setTimeoutsetInterval,用于调度DOM修改函数,委托 执行到孩子并立即返回。 此步骤可以自动清理!
  3. 应将儿童持久的DOM操作分成更快的块 使用setTimeoutsetInterval,以便不锁定事件循环 需很长时间。 这为两个窗口提供了响应
  4. 最简单的儿童javascript示例。从父进程调用的函数是LongLastingOperation,它已被拆分为4个块:

    <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>