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之后就有了这篇文章。
首先解释一下这里的同源是什么意思。千万不要把浏览器的同源策略混起来,这里的同源和那里同域是两回事。
同源就是下文中指的浏览器实例:

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

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

二 正文

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

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

谷歌浏览器进程的模型

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

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

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

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

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

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

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

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

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

窗口之间同步通信

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

我们将一组用相互用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",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,

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

补充二:如何优化

dawei

【声明】:唐山站长网内容转载自互联网,其相关言论仅代表作者个人观点绝非权威,不代表本站立场。如您发现内容存在版权问题,请提交相关链接至邮箱:bqsm@foxmail.com,我们将及时予以处理。