Shared Event-loop for Same-Origin Windows(译)

一 前言

在翻译这篇文章Tasks, microtasks, queues and schedules时,有一个不懂之处:"All windows on the same origin share an event loop as they can synchronously communicate."Google以后就有了这篇文章。
首先解释一下这里的同源是什么意思。千万不要把浏览器的同源策略混起来,这里的同源和那里同域是两回事。
同源就是下文中指的浏览器实例:javascript

咱们将一组用相互用script链接的Tabs称为一个浏览实例,它对应于HTML5规范中的“相关的浏览上下文单元”。 该组由一个选项卡和任何其余使用Javascript代码打开的选项卡组成。

例若有ABC三个页面,在A中执行window.open(B),那么AB就是同源,B又打开了C,则ABC就是同源。html

二 正文

原文地址:http://hassansin.github.io/sh...前端

我最近看到一篇文章上说:“来自同一个源的全部窗口共享一个事件循环,它们也能够同步通讯。” 照它这么说 -- 若是我在浏览器上打开了多个Tab(由于选项卡与现代浏览器中的窗口基本相同),Tab来自同一主机的不一样页面,它们全都将呈如今单个线程中。 可是这没根本不可能吧,由于Chrome在其本身的进程中运行每一个Tab。 他们没法共享相同的事件循环。 文章说法有待考证。java

谷歌浏览器进程的模型

使用chrome任务管理器的快速测试证实我是正确的。每一个具备来自相同域的Tab确实是在单独的进程中运行。 可是当我在Chrome任务管理器进程中进行挖掘时,我注意到一些Tab是在相同进程ID下运行的。 例如:git

chrome-task-manager-1.png

Tabs甚至不是来自同一个域却在同一个进程里面。 因此这里发生了什么? 谷歌快速搜索后,事实证实,Chrome有一个复杂的流程模型:chromium.org/developers/design-documents/process-models。 默认状况下,Chrome使用process-per-site-instance模型,即:github

Chromium会为用户访问的每一个站点实例建立一个渲染器进程。 这确保了来自不一样站点的页面被独立渲染,而且对同一站点的单独访问也彼此隔离。

所以,一个站点的一个实例中的失败(例如渲染器崩溃)或资源占用率太高不会影响浏览器的其他部分。 

**该模型基于内容的源和相互执行脚本的选项卡之间的关系。** 所以,两个选项卡可能会显示在任务管理器的同一个进程中,而当在已经打开的一个页面的选项卡中导航到跨站页面时,可能会切换选项卡的渲染器进程。

但事实上,我认为实际状况比上述内容更复杂。 Ctrl + click 打开来自同一页面的不一样连接有时会在同一个进程中打开这些连接,有时不会 -- 无论它们的域是什么。web

无论那些了,我迫切地想测试一下这些Tab是否真的共享相同的Event Loop。 因此我写了一个长时间运行的同步任务。 你猜怎么了! 这只是一个空循环:chrome

function longrunning(){
    for (let i=0; i<10000000000; i++);
}

而后,我须要将其注入到这些tabs-per-process的其中一个中去。 有一个很好的扩展称为Custom JavaScript for websites能够作到这一点。 当我使用此扩展插入脚本并运行它时,它将进程上的全部选项卡都挂起了。 任务完成。 我还历来没有这么高兴地看到了像这样被挂起地页面。segmentfault

窗口之间同步通讯

回到我刚才讨论的第一篇文章。 它也提到这些窗口还能够同步进行相互通讯。 因此这些Tab必须以某种方式相互链接。 从关于Chrome进程模型的文章:windows

咱们将一组用相互用script链接的Tabs称为一个浏览实例,它对应于HTML5规范中的“相关的浏览上下文单元”。 该组由一个选项卡和任何其余使用Javascript代码打开的选项卡组成。 

这些选项卡必须在同一个进程中呈现,以容许在它们之间进行Javascript调用(最多见的是来自同一源的页面之间)。

好吧,这意味着咱们须要使用JavaScript打开它们,才能链接窗口。 实际上有几种方法能够在javascript中执行此操做。 使用 iframewindow.frameswindow.open。 而且要相互通讯的话,咱们可使用window.postMessage web api。 咱们还能够轻松测试使用window.open打开的选项卡是否共享相同的事件循环。 我准备了这个演示页面,使用window.open打开一个弹出窗口。 而后,顶部窗口和子窗口都运行一些同步任务,咱们能够看到它们是如何相互影响的。

演示在这里demo。 你须要让浏览器容许弹出窗口才能看到效果。

top.html:

<html>
<head>
<title>Top window</title>
<script>
    
        function longrunning(){
            for(let i=0;i<2000000000;i++);
        }
        let t0
        let t1
        const elapsedTime = () => {
            if(!t0) {
                t0 = performance.now()
                t1 = t0
            } else {
                t1 = performance.now()
            }
            return ((t1-t0)/1000).toFixed(2)
        }
        window.parentLogger = (str) => {
            console.log("[%s] TOP: %s", elapsedTime(), str)
        }
        window.childLogger = (str) => {
            console.log("[%s] CHILD: %s", elapsedTime(), str)
        }

        parentLogger('before opening popup')
        const popup = window.open('child.html');
        // var popup = window.open('/child.html', '', 'noopener=true');
        if(popup) {
            parentLogger(`after popup opened, popup window url: ${popup.location.href}`)
        }

        parentLogger('starting long synchronous process. This will prevent loading and parsing of popup window')
        longrunning();
        parentLogger('finished long synchronous process.')

        parentLogger('adding 1s timeout.')
        setTimeout(function(){
            parentLogger('timed out')
        },1000)
    
</script>
</head>
<body></body>
</html>

child.html:

<html>
<head>
<title>Child window</title>
<script>
        function longrunning(){
            for(let i=0;i<2000000000;i++);
        }
        window.addEventListener('DOMContentLoaded', e => window.opener.childLogger(`popup initial html loaded, popup window url: ${window.location.href}`))
        window.opener.childLogger('starting long synchronous process inside popup window. This will prevent the event loop in top window')
        longrunning()
        window.opener.childLogger('finished long synchronous process inside popup window.')
        // window.close()
  </script> 
  </head>
  <body></body>
  </html>

不过,这里有top.html中控制台的输出:

[0.00] TOP: before opening popup
[0.01] TOP: after popup opened, popup window url: about:blank
[0.01] TOP: starting long synchronous process. This will prevent loading and parsing of popup window
[4.93] TOP: finished long synchronous process.
[4.93] TOP: adding 1s timeout.
[5.82] CHILD: starting long synchronous process inside popup window. This will prevent the event loop in top window
[10.79] CHILD: finished long synchronous process inside popup window.
[11.15] CHILD: popup initial html loaded, popup window url: http://localhost:4000/assets/chrome-process-models/child.html
[11.18] TOP: timed out

你能够在每一个事件的方括号中查看以秒计的总时间。 TOP表示它是从父窗口记录的,而CHILD表示它是从弹出窗口记录的。如下是发生了什么事情的简要介绍:

  1. 打开弹出窗口是同步的,但弹出窗口中的内容是异步加载的。这就是为何当咱们在window.open以后检查弹出的URL时,它被设置为about:blank。实际上URL的获取被延迟,并在当前脚本块执行完成后开始
  2. 接下来,咱们在顶部窗口中运行长时间运行的任务。这会阻止事件循环和任何pedding的回调。所以,在同步过程完成以前,弹出窗口中的内容将没法加载。
  3. 而后咱们在顶部窗口中添加1秒的超时时间。这将完成顶部窗口中的当前脚本块。这意味着如今弹出窗口将有机会加载其内容。
  4. 弹出窗口将开始加载内容并执行它看到的任何JavaScript代码。在弹出窗口内容的顶部,咱们再次开始一个长时间运行的任务。只要它正在运行,它就会阻止任何待执行的回调。这意味着咱们在顶部窗口的1秒超时也会延迟。
  5. 接下来咱们看到DOMContentLoaded事件是针对弹出窗口触发的。当初始HTML文档已被彻底加载和解析时,此事件被触发,而无需等待样式表,图像和子帧完成加载。
  6. 最后咱们看到在6秒后约1秒的超时回调被触发。

因此从弹出窗口中加载内容的时间点以及在顶部窗口中触发setTimeout回调的时间点能够清楚地看到,它们都共享相同的事件循环。

那么咱们如何让同源窗口在它本身的进程中运行而不影响彼此的事件循环呢? 事实证实,咱们能够在window.open()中传递一个选项noopener。 可是使用该选项也会失去对父窗口的引用。 因此咱们没法使用window.postMessage()在窗口之间进行通讯。


全部这些行为在不一样的浏览器中可能会有所不一样。 这实际上都是特定于浏览器的实现。 咱们甚至能够在Chrome中传递不一样的标志并选择不一样的过程模型。

三 后记

这篇文章给出了最终答案:来自同一个源的Tabs共享相同的事件循环。
可使用JS调用的方式打开(例如window.open)的Tabs建立同一源,即便这些Tabs不一样域。
始终强调一点须要注意:全部这些行为在不一样的浏览器中可能会有所不一样。

补充一:同一进程

打开chrome的任务管理,能够看到任务状况。
例如我在当前页面控制台执行了一个

window.open('https://segmentfault.com/a/1190000014833359');

打开了一个新的Tab,可是:

图片描述

在同一个进程里。
我又在当前页面控制台执行了一个

window.open('https://www.baidu.com');

又打开了一个新的Tab,

图片描述

嗯,仍是同一个进程。
这里说明了共享事件循环的可行性。

补充二:如何优化

多个Tabs共享相同的事件循环确定会相互影响,除了使用在window.open()中传递一个选项noopener的方法外,咱们要注意尽可能减小使用window.open,使用a标签就不会出现这样的问题。固然咱们还能够在适当的时候调用window.close()关闭不须要的Tab。

补充三:跨域通讯

文章讲到,它们能够同步通讯。固然,不多使用window.open的方式来相互通讯,可是ifame倒是很经常使用的 -- ifram中能够加载别的域的页面。在建立了iframe以后是能够拿到ifame的实例的。而后就可使用postMessage相互通讯了。

postMessage解决了:

a.) 页面和其打开的新窗口的数据传递
b.) 多窗口之间消息传递
c.) 页面与嵌套的iframe消息传递
d.) 上面三个场景的跨域数据传递

下面是一个实际的例子:

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };

    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

更加全面的跨域知识请看:前端常见跨域解决方案(全)

三 附录

Process-per-site-instance

By default, Chromium creates a renderer process for each instance of a site the user visits. This ensures that pages from different sites are rendered independently, and that separate visits to the same site are also isolated from each other. Thus, failures (e.g., renderer crashes) or heavy resource usage in one instance of a site will not affect the rest of the browser. This model is based on both the origin of the content and relationships between tabs that might script each other. As a result, two tabs may display pages that are rendered in the same process, while navigating to a cross-site page in a given tab may switch the tab's rendering process. (Note that there are important caveats in Chromium's current implementation, discussed in the Caveats section below.)

Concretely, we define a "site" as a registered domain name (e.g., google.com or bbc.co.uk) plus a scheme (e.g., https://). This is similar to the origin defined by the Same Origin Policy, but it groups subdomains (e.g., mail.google.com and docs.google.com) and ports (e.g., http://foo.com:8080) into the same site. This is necessary to allow pages that are in different subdomains or ports of a site to access each other via Javascript, which is permitted by the Same Origin Policy if they set their document.domain variables to be identical.

A"site instance" is a collection of connected pages from the same site. We consider two pages as connected if they can obtain references to each other in script code (e.g., if one page opened the other in a new window using Javascript).

相关文章
相关标签/搜索