【问题标题】:closing a window breaks event loop assumption关闭窗口会破坏事件循环假设
【发布时间】:2013-02-15 13:28:24
【问题描述】:

我遇到了一个小烦恼,后来演变成一个大问题。

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

这意味着任何对 DOM 的调用,例如 appendChildcreateElement,都会失败并返回 SCRIPT70: Permission DeniedSCRIPT1717: The interface is unknown

我查看了其他浏览器(例如 Chrome)的行为。在 Chrome 中 ownerDocument 仍然引用 #documentownerDocument.defaultView 最终将是 undefined。这对我来说很有意义。对appendChildcreateElement 的调用将通过。只要您不尝试直接引用defaultView,我认为一切都很好。

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

如果我们只是遇到问题 1,将会有一个 - 仍然很痛苦但 - 直接的解决方案:检查 ownerDocument 是否存在,如果不存在则跳过。因为它是 ownerDocument 在同步 JavaScript 代码中间消失。

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

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

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

糟糕的解决方案: 将任何与 DOM 交互的函数包装在一个函数中,如果它检测到元素的窗口已关闭,则会消耗错误。这是相当侵入性的,对性能有很大影响,而且 IE 已经很慢了。

有没有更好的解决方案?

我想要的至少是一种忽略由于用户关闭窗口而引发的任何Errors 的方法。 问题 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/

【问题讨论】:

  • 所有 IE 版本都会出现这种情况吗?
  • 出现在 IE9 中。我不敢测试旧版本。我不知道IE10。
  • Chrome 允许您修改关闭窗口的 DOM,只要您持有对它的引用,我是否理解正确?你想在 IE 中实现这种行为吗?
  • ownerDocument 在同步 JavaScript 代码中间消失了” - 你怎么有这么长时间运行的同步代码,你可以在执行期间关闭一个窗口?
  • @Bergi 是的,您可以在 Chrome 中保留对 DOM#document 的引用(据我所知,无限期)。 “长时间运行”是相对的,但 IE9 很慢。在 Chrome 中运行不到 2 秒的脚本在 IE9 中大约需要 20 秒。只需重复这些步骤,我就可以轻松地延长执行时间。当我查看 Chrome 中的行为时,我发现它确实尊重事件循环的预期行为。 Window 可能会在视觉上消失,但 JavaScript 在完成较早的事件之前不会收到 unload 事件(应该如此)。

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


【解决方案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 中获取文件名和行号。再一次,一个严重的疏忽最终只会浪费每个人的时间。

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • 这不起作用。当unload 事件发生时,另一个事件循环正在处理一些其他功能(如制作 DOM 节点和设置属性)。您不能追溯包装已被调用的函数。
    • 然后您可以确保推入事件循环的函数调用其他为您执行 DOM 操作的函数,而无需自己进行任何 DOM 操作。然后,您可以使用unload 事件重新定义执行 DOM 操作的函数,然后再从事件循环中的函数调用它们。
    • 我理解你的建议,我已经尝试过了,但没有成功。我尝试跟踪我制作的所有 DOM 元素,然后用空函数替换它们的函数,用虚拟属性替换属性。它没有任何效果。你以前试过这个吗?你有一个有效的解决方案吗?理论上我同意它应该可以工作,但理论上我也不应该遇到这个问题。
    • 您能否发布一个引发您提到的Permission Denied 错误的示例?我应该能够根据我的想法进行修改。
    • 查看您的示例后,您的解决方案似乎是最好的。
    【解决方案3】:

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

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

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

    我们有 3 个组件:parent-ie.html、child-ie.html 和小的 parallel.js 文件。一个怪癖是,所有浏览器都使用了 setInterval(function, -1)。正值是制动 IE,第二个省略的参数使 Opera 感到困惑,因此它只产生了第一个功能块。总之,代码在这里:

    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 是从 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);
        }
    }
    

    就是这样。

    【讨论】:

    • 很抱歉在这里打破你的泡沫,但如果我有一个 child-ie.html 我不会有这个问题,因为我可以把所有的处理放到子窗口中,当你关闭时它会自动清理干净它。是这样还是这个解决方案还有更多? setInterval 当然有它自己的性能考虑。
    • 您假设我有一个类似迭代器的过程,即。作为一个迭代过程很容易编写的东西。演示确实如此,但这是为了证明 IE9 不可原谅的行为而过于简单化了。我很感激你的努力,但我觉得你在这里有点忘乎所以。
    • 是的,还有更多。如果你只是把你的循环放在子窗口中,它会阻塞父窗口直到操作完成,即使它已经关闭。正如您所提到的,原因是他们让操作在关闭的窗口上继续进行。 IE 会抛出异常。因此,简单地将您的代码移动到孩子不会做任何有用的事情。 setInterval 让我们“事件循环”来呼吸。
    • 关于 for 循环,这只是最终可重用的示例。可能是while循环或其他任何东西。您可以将此应用于任何功能。很难相信你有一些无法重构的东西,移动到适当的窗口切成块。所以,模式是 move-code-to-owner-window+setInterval 一起使用。组合。
    • 我的代码是同步的,而不是异步的,甚至不是类似迭代器的,这是有充分理由的。我不能在不披露敏感信息的情况下透露原因。我能说的是,这是我的应用程序中出现的一个边缘案例,它非常罕见而且非常微不足道。我很难相信你觉得这很难相信..
    【解决方案4】:

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

    1. 父级不应直接修改子 DOM。函数应该在 孩子本身。 但是,仅此一项并不能解决任何问题,如果 函数从父级触发并同步执行。 挂起的窗口和异常仍然存在。
    2. 父调用子的主函数必须使用setTimeoutsetInterval,调度 DOM 修改函数,委托 处决孩子并立即返回。 这一步使自动清理成为可能!
    3. 应将子项中持久的 DOM 操作拆分为更快的块 使用setTimeoutsetInterval,这样事件循环就不会被锁定 需很长时间。 这可以响应两个窗口

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多