- 发布于
浏览器运行机制
- Authors
- Name
- 田中原
浏览器运行机制
一、计算机的核心 CPU 与 GPU
为了了解浏览器运行的环境,我们需要熟悉几个计算机部件以及它们的作用。
1、CPU
中央处理器(Central Processing Unit),或简称为 CPU。CPU 可以看作是计算机的大脑。一个 CPU 核心如图中的办公人员,可以逐一解决很多不同任务。它可以在解决从数学到艺术一切任务的同时还知道如何响应客户要求。过去 CPU 大多是单芯片的,一个核心就像存在于同芯片的另一个 CPU。随着现代硬件发展,你经常会有不止一个内核,为你的手机和电脑提供更多的计算能力。
[图:4 个 CPU 核心作为办公人员,坐在办公桌前处理各自的工作]
2、GPU
图形处理器(Graphics Processing Unit,简称为 GPU)是计算机的另一重要部件。与 CPU 不同,GPU 擅长同时处理跨内核的简单任务。顾名思义,它最初是为解决图形而开发的。这就是为什么在图形环境中“使用 GPU” 或 “GPU 支持”都与快速渲染和顺滑交互有关。近年来随着 GPU 加速计算的普及,仅靠 GPU 一己之力也使得越来越多的计算成为可能。
[图:许多带特定扳手的 GPU 内核意味着它们只能处理有限任务]
3、作用
当你在电脑或手机上启动应用时,是 CPU 和 GPU 为应用供能。通常情况下应用是通过操作系统提供的机制在 CPU 和 GPU 上运行。下图为计算机三层体系结构图,底部是机器硬件,中间是操作系统,顶部是应用程序。
[图:三层计算机体系结构。底部是机器硬件,中间是操作系统,顶部是应用程序]
二、进程和线程
在深入学习浏览器架构之前需要了解一下进程与线程。进程可以被描述为是一个应用的执行程序。线程存在于进程中并执行程序任意部分。
- 进程是一个工厂,工厂有它的独立资源
【工厂的资源 -> 电脑系统分配的内存(独立的一块内存)】
【工厂之间的相互独立 -> 进程之间相互独立】
- 线程是工厂中的工人,多个工人协作完成任务
【多个工人协作完成任务 -> 多个线程在进程中协作完成任务】
【工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成】
- 工人之间共享空间
【同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)】
[图:进程作为边界框,线程作为抽象鱼在进程中游动]
1、查看 “进程” 方法
Window 系统
**打开任务管理器后,有一个后台进程列表。**可以看到每个进程的内存资源信息以及CPU、内存、磁盘、网络等占有率。
Mac OS 系统
打开活动监视器,可以看到有一个后台进程列表。这个列表展示了每个进程的CPU、内存、能耗、磁盘、网络等资源信息。
启动应用时会创建一个进程。程序也许会创建一个或多个线程来帮助它工作,这是可选的。电脑操作系统为进程提供了一个可以使用的“一块”内存,所有应用程序状态都保存在该私有内存空间中。关闭应用程序时,相应的进程也会消失,操作系统会释放内存。
[图:进程使用内存空间和存储应用数据的示意图]
进程可以请求操作系统启动另一个进程来执行不同的任务。此时,内存中的不同部分会分给新进程。如果两个进程需要对话,他们可以通过进程间通信(IPC)来进行。许多应用都是这样设计的,所以如果一个工作进程失去响应,该进程就可以在不停止应用程序不同部分的其他进程运行的情况下重新启动。
[图:独立进程通过 IPC 通信示意图]
2、总结
- 进程是CPU资源分配的最小单位(电脑系统会给它分配内存,因此是能拥有资源和独立运行的最小单位)
- 线程是CPU调度的最小单位(它是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
- 不同进程之间也可以通信,不过代价较大。
- 现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多。(所以核心还是得属于一个进程才行)
三、浏览器架构【Chrome 浏览器】(重点)
那么如何通过进程和线程构建 web 浏览器呢?它可能由一个拥有很多线程的进程,或是一些通过 IPC 通信的不同线程的进程。
[图:Chrome浏览器架构的进程/线程示意图]
这里需要注意的重要一点是,这些不同的架构是实现细节。关于如何构建 Web 浏览器并不存在标准规范。一个浏览器的构建方法可能与另一个迥然不同。我们这里主要介绍 Chrome 浏览器 的架构。
浏览器是 多进程 的
浏览器之所以能够运行,是因为系统给它的进程分配了资源(CPU、内存)
简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。
注意:在这里浏览器应该也有自己的优化机制,有时候打开多个tab页后,可以在Chrome任务管理器中看到,有些进程被合并了 (所以每一个Tab标签对应一个进程并不一定是绝对的,比如打开多个空白标签页后,会发现多个空白标签页被合并成了一个进程)
关于上面几点的验证,上图中打开了Chrome浏览器
的多个标签页,然后可以在 Chrome的任务管理器
中看到有多个进程(分别是每一个Tab页面有一个独立的进程,以及一个主进程)。
1、浏览器都包含哪些进程
Browser进程【Browser Process】:浏览器的主进程(负责协调、主控),只有一个。
- 负责浏览器界面显示,与用户交互。如前进,后退等
- 负责各个页面的管理,创建和销毁其他进程
- 将 Renderer 进程得到的内存中的 Bitmap 【位图】,绘制到用户界面上
- 以及处理 web 浏览器不可见的特权部分,如网络请求和文件访问等
GPU进程【GPU Process】:最多一个,用于3D绘制等
- 处理独立于其它进程的 GPU 任务。GPU 被分成不同进程,因为 GPU 处理来自多个不同应用的请求并绘制在相同表面。
浏览器渲染进程【Renderer Process】:默认每个Tab页面一个进程,互不影响。
- 它是浏览器内核
- 内部是多线程的
- 负责页面渲染、展示,脚本执行,事件处理等
第三方插件进程【Plugin Process】:每种类型的插件【扩展程序】对应一个进程,仅当使用该插件时才创建
2、浏览器多进程的优势
最简单的情况下,你可以想象每个标签页都有自己的渲染进程。假设你打开了三个标签页,每个标签页都拥有自己独立的渲染进程。如果某个标签页失去响应,你可以关掉这个标签页,此时其它标签页依然运行着,可以正常使用。**如果所有标签页都运行在同一进程上,那么当某个失去响应,所有标签页都会失去响应。**这样就没办法玩了!!!
[图:如图所示每个标签页上运行的渲染进程]
把浏览器工作分成多个进程的另一好处是安全性与沙箱化。由于操作系统提供了限制进程权限的方法,浏览器就可以用沙箱保护某些特定功能的进程。例如,Chrome 浏览器限制处理任意用户输入的进程(如渲染器进程)对任意文件的访问。
由于进程有自己的私有内存空间,所以它们通常包含公共基础设施的拷贝(如 V8,它是 Chrome 的 JavaScript 引擎)。这意味着使用了更多的内存,如果它们是同一进程中的线程,就无法共享这些拷贝。为了节省内存,Chrome 对可加速的内存数量进行了限制。具体限制数值依设备可提供的内存与 CPU 能力而定,但是当 Chrome 运行时达到限制时,会开始在同一站点的不同标签页上运行同一进程。
相比于单进程浏览器,多进程有如下优点:
- 避免单个page crash【页面崩溃】影响整个浏览器
- 避免第三方插件 crash【崩溃】影响整个浏览器
- 多进程充分利用多核优势
- 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性
3、浏览器内核(渲染进程)
重点来了,我们可以看到,上面提到了这么多的进程,那么,对于普通的前端操作来说,最终要的是什么呢?答案是渲染进程。可以这样理解,页面的渲染,JS的执行,事件的循环,都在这个进程内进行。接下来重点分析这个进程。
请牢记,浏览器的渲染进程是多线程的。
- GUI渲染线程【页面渲染】
- 负责渲染浏览器界面,解析HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。
- 当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行
- 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。
- JS引擎线程【也称为JS内核】
- JS 引擎线程负责解析 Javascript 脚本程序(例如V8引擎),运行代码。
- JS 引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(Renderer进程)中无论什么时候都只有一个 JS 线程在运行JS程序
- 同样注意,JS引擎线程与GUI渲染线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
- 事件触发线程
- 归属于浏览器而不是 JS 引擎,用来控制事件循环(可以理解,JS 引擎自己都忙不过来,需要浏览器另开线程协助)
- 当 JS引擎执行代码块如 setTimeout 时(也可来自浏览器内核的其他线程,如鼠标点击、ajax 异步请求等),会将对应任务添加到事件线程中
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
- 注意,由于 JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS引擎处理(当JS引擎空闲时才会去执行)
- 定时触发器线程
- 传说中的
window.setInterval
与window.setTimeout
所在线程 - 浏览器定时计数器并不是由 JavaScript 引擎计数的,(因为JavaScript引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确)
- 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
- 注意,W3C在HTML标准中规定,规定要求 setTimeout 中低于 4ms 的时间间隔算为 4ms。
- 传说中的
- 异步http请求线程
- 在XMLHttpRequest 在连接后是通过浏览器新开一个线程请求
- 将检测到状态变更时,如果设置有回调函数,异步线程就 产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
放张图巩固下吧:
4、Browser 进程和浏览器内核(Renderer进程)的通信过程
首先,应该对浏览器内的进程和线程都有一定理解了,那么接下来,再谈谈浏览器的Browser进程(控制进程)是如何与内核通信的, 这点也理解后,就可以将这部分的知识串联起来,从头到尾有一个完整的概念。
如果打开任务管理器,然后打开一个浏览器,就可以看到:任务管理器中出现了两个进程(一个是主控进程,一个则是打开Tab页的渲染进程), 然后在这前提下,看下整个的过程:(简化了很多)
- Browser进程收到用户请求,首先需要获取页面内容(譬如通过网络下载资源),随后将该任务通过RendererHost 接口传递给 Renderer 进程
- Renderer 进程的 Renderer 接口收到消息,简单解释后,交给GUI渲染线程,然后开始渲染
- GUI渲染线程接收请求,加载网页并渲染网页,这其中可能需要 Browser 进程获取资源和需要 GPU 进程来帮助渲染
- 当然可能会有 JS引起线程 操作 DOM(这样可能会造成回流并重绘)
- 最后 Renderer 进程将结果传递给 Browser 进程
- Browser 进程接收到结果并将结果绘制出来
四、浏览器内核中线程之间的关系
1、GUI渲染线程与JS引擎线程互斥
由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JS线程和 GUI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JS 引擎为互斥的关系,当 JS 引擎执行时 GUI线程会被挂起, GUI 更新则会被保存在一个队列中等到 JS 引擎线程空闲时立即被执行。
2、JS阻塞页面加载
从上述的互斥关系,可以推导出,JS 如果执行时间过长就会阻塞页面。
譬如,假设JS引擎正在进行巨量的计算,此时就算GUI有更新,也会被保存到队列中,等待JS引擎空闲后执行。 然后,由于巨量计算,所以JS引擎很可能很久很久后才能空闲,自然会感觉到巨卡无比。
所以,要尽量避免JS执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
3、WebWorker,JS 的多线程?
前文中有提到 JS 引擎是单线程的,而且 JS 执行时间过长会阻塞页面,那么 JS 就真的对 CPU 密集型计算无能为力么?所以,后来HTML5中支持了WebWorker
。
Web Worker 为Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。一个 worker 是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的 JavaScript 文件。 这个文件包含将在工作线程中运行的代码; workers 运行在另一个全局上下文中,不同于当前的window。因此,使用 window快捷方式获取当前全局的范围 (而不是self) 在一个 Worker 内将返回错误。
这样理解下:
- 创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
- JS引擎线程与 Worker 线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
所以,如果有非常耗时的工作,请单独开一个Worker线程,这样里面不管如何翻天覆地都不会影响JS引擎主线程, 只待计算出结果后,将结果通信给主线程即可。
而且注意下,JS引擎是单线程的,这一点的本质仍然未改变,Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题。
4、WebWorker与SharedWorker
既然都到了这里,就再提一下SharedWorker
(避免后续将这两个概念搞混)
- WebWorker 只属于某个页面,不会和其他页面的 Renderer 进程(浏览器内核进程)共享
- 所以 Chrome 在 Renderer 进程中(每一个 Tab 页就是一个 Renderer 进程)创建一个新的线程来运行 Worker中的 JavaScript 程序。
- SharedWorker 是浏览器所有页面共享的,不能采用与 WebWorker 同样的方式实现,因为它不隶属于某个Renderer 进程,可以为多个 Renderer 进程共享使用
- 所以Chrome浏览器为SharedWorker单独创建一个进程来运行JavaScript程序,在浏览器中每个相同的JavaScript只存在一个SharedWorker进程,不管它被创建多少次。
看到这里,应该就很容易明白了,本质上就是进程和线程的区别。SharedWorker由独立的进程管理,WebWorker 只是属于 Renderer 进程下的一个线程。
五、浏览器渲染流程
让我们看一个网络浏览的简单用例:你在浏览器中键入 URL,然后浏览器从互联网获取数据并显示一个页面。在这篇文章中,我们将重点放在用户请求站点 和 浏览器准备渲染页面部分 —— 亦即导航。
[图:顶部是浏览器 UI,底部是拥有 UI、网络和存储线程的浏览器进程图]
正如CPU、GPU、内存和多进程架构中所述,tab 外的一切都被浏览器进程处理。浏览器进程有很多线程,例如 绘制浏览器按钮和输入栏的 GUI 线程、处理网络栈以从因特网获取数据的网络线程、控制文件访问的存储线程等。当你在地址栏中键入 URL 时,你的输入将由浏览器进程的 GUI 线程处理。
第 1 步:处理输入
当用户开始在地址栏键入时,GUI 线程要问的第一件事是 “这是一次搜索查询还是一个 URL 地址?”。在 Chrome 中,地址栏同时也是一个搜索输入栏,所以 GUI 线程需要解析和决定把你的请求发送到搜索引擎,或是你要请求的网站。
[图:GUI 线程询问输入内容是搜索查询还是 URL 地址]
第 2 步:开始导航
当用户按下 Enter 键时,GUI 线程启用网络调取去获取站点内容。加载动画会显示在标签页的一角,网络线程会通过适当的协议,像 DNS 查找和为请求建立 TLS 连接。【Http协议 3 次握手过程 】
[图:GUI 线程告诉网络线程要导航到 mysite.com]
在这时,网络线程可能会收到像 HTTP 301 那样的服务器重定向头。这种情况下,网络线程会告诉 GUI 线程,服务器正在请求重定向。然后,另一个 URL 请求会被启动。
第 3 步:读取响应
[图:包含 Content-Type 的响应头以及作为实际数据的 payload]
一旦开始收到响应主体(payload),网络线程会在必要时查看数据流的前几个字节。响应报文的 Content-Type 字段会声明数据的类型,但是它有可能会丢失或者错误,所以就有了 MIME 类型嗅探来解决这个问题。这是源码中评论的“棘手的问题”。你可以阅读注释看一下不同浏览器是怎么匹配 content-type 和 payload 的。
如果响应是一个 HTML 文件,那么下一步就会把数据传给渲染进程,但是如果是一个压缩文件或是其他文件,那么意味着它是一个下载请求,因此需要将数据传递给下载管理器。
[图:网络线程询问一个响应数据是否是从安全网站来的 HTML]
此时也会进行 SafeBrowsing 检查。如果域名和响应数据似乎匹配到一个已知的恶意网站,那么网络线程会显示一个警告页面。除此之外,还会发生 Cross Origin Read Blocking(CORB)检查,以确保敏感的跨域数据不被传给渲染进程。
第 4 步:查找渲染进程
一旦所有的检查执行完毕并且网络线程确信浏览器会导航到请求的站点,网络线程会告诉 GUI 线程所有的数据准备完毕。GUI 线程会寻找渲染进程去开始渲染 web 页面。
[图:网络线程告诉 GUI 线程去查找渲染进程]
由于网络请求会花费几百毫秒才获取回响应,因此可以应用一个优化措施。当第 2 步 GUI 线程正发送一个 URL 请求给网络线程时,它已经知道它们会导航到哪个站点。在网络请求的同时,GUI 并行地线程尝试主动寻找或开启一个渲染进程。这样,如果一切按预期进行,渲染进程在网络线程接受到数据时就已经处于待命状态。如果导航跨域重定向,这个待命进程也许不会被用到,这种情况下也许会用到另一个进程。
第 5 步:提交导航
现在数据和渲染进程已经就绪,浏览器进程会发送一个 IPC(进程间通信)到渲染进程去提交导航。它也会传递数据流,所以渲染进程可以保持接收 HTML 数据。一旦浏览器进程收到渲染进程已经提交的确认消息,导航完毕并且文档加载解析开始。
浏览器器内核拿到内容后,渲染大概可以划分成以下几个步骤:
- 解析html建立dom树
- 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
- 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
- 绘制render树(paint),绘制页面像素信息
- 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。
所有详细步骤都已经略去,渲染完毕后就是
load
事件了,之后就是自己的JS逻辑处理了。
load事件与DOMContentLoaded事件的先后
上面提到,渲染完毕后会触发
load
事件,那么你能分清楚load
事件与DOMContentLoaded
事件的先后么?很简单,知道它们的定义就可以了:- 当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片。 (譬如如果有async加载的脚本就不一定完成)
- 当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了。 (渲染完毕了)
所以,顺序是:
DOMContentLoaded -> load
css加载是否会阻塞dom树渲染?
这里说的是头部引入css的情况。首先,我们都知道:css是由单独的下载线程异步下载的。
然后再说下几个现象:
css加载不会阻塞DOM树解析(异步加载时DOM照常构建)
但会阻塞render树渲染(渲染时需等css加载完毕,因为render树需要css信息)
这可能也是浏览器的一种优化机制。因为你加载css的时候,可能会修改下面 DOM 节点的样式, 如果 css 加载不阻塞 render 树渲染的话,那么当 css 加载完之后, render 树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。 所以干脆就先把 DOM 树的结构先解析完,把可以做的工作做完,然后等你 css 加载完之后, 在根据最终的样式来渲染 render 树,这种做法性能方面确实会比较好一点。
普通图层和复合图层
渲染步骤中就提到了
composite
概念。可以简单的这样理解,浏览器渲染的图层一般包含两大类:
普通图层
以及复合图层
首先,普通文档流内可以理解为一个复合图层(这里称为
默认复合层
,里面不管添加多少元素,其实都是在同一个复合图层中)其次,absolute布局(fixed也一样),虽然可以脱离普通文档流,但它仍然属于
默认复合层
。然后,可以通过
硬件加速
的方式,声明一个新的复合图层
,它会单独分配资源 (当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层
里的回流重绘)
可以简单理解下:GPU中,各个复合图层是单独绘制的,所以互不影响,这也是为什么某些场景硬件加速效果一级棒,可以
Chrome源码调试 -> More Tools -> Rendering -> Layer borders
中看到,黄色的就是复合图层信息如下图。可以验证上述的说法
如何变成复合图层(硬件加速)
将该元素变成一个复合图层,就是传说中的硬件加速技术
- 最常用的方式:
translate3d
、translateZ
- opacity
属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
will-chang
属性(这个比较偏僻),一般配合opacity与translate使用(而且经测试,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层), 作用是提前告诉浏览器要变化,这样浏览器会开始做一些优化工作(这个最好用完后就释放)<video><iframe><canvas><webgl>
等元素其它,譬如以前的flash插件
- 最常用的方式:
absolute和硬件加速的区别
可以看到,absolute虽然可以脱离普通文档流,但是无法脱离默认复合层。 所以,就算absolute中信息改变时不会改变普通文档流中render树, 但是,浏览器最终绘制时,是整个复合层绘制的,所以absolute中信息的改变,仍然会影响整个复合层的绘制。 (浏览器会重绘它,如果复合层中内容多,absolute带来的绘制信息变化过大,资源消耗是非常严重的)
而硬件加速直接就是在另一个复合层了(另起炉灶),所以它的信息改变不会影响默认复合层 (当然了,内部肯定会影响属于自己的复合层),仅仅是引发最后的合成(输出视图)
复合图层的作用?
一般一个元素开启硬件加速后会变成复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能
但是尽量不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡
硬件加速时请使用index
使用硬件加速时,尽可能的使用index,防止浏览器默认给后续的元素创建复合层渲染
具体的原理时这样的: webkit CSS3中,如果这个元素添加了硬件加速,并且index层级比较低, 那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且releative或absolute属性相同的), 会默认变为复合层渲染,如果处理不当会极大的影响性能
简单点理解,其实可以认为是一个隐式合成的概念:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层,这点需要特别注意
这时,地址栏已经更新,安全指示器和站点设置 GUI 会反映新页面的站点信息。此标签页的 session 历史记录会被更新,所以前进/后退按钮会走向刚导航过的站点。当你关闭标签页或者窗口,为了优化 tab/session 的还原,session 历史被保存在硬盘上。
[图:浏览器和渲染进程间的 IPC,请求渲染页面]
额外的步骤:初始加载完毕
一旦导航被提交,渲染进程开始加载资源和渲染页面。一旦渲染进程渲染“完毕”。它会发送一个 IPC 返回给浏览器进程(这会在页面所有的 frame 的 onload
事件已经触发和执行完毕后发生)。这时,GUI 线程停止标签页上的加载动画。
我之所以说“结束”,是因为客户端 JavaScript 可以在这时之后仍然加载额外的资源并且渲染新视图。
[图:渲染进程发送 IPC 到浏览器进程通知页面“已被加载”]
从Event Loop谈JS的运行机制
到此时,已经是属于浏览器页面初次渲染完毕后的事情,JS引擎的一些运行机制分析。
注意,这里主要是结合
Event Loop
来谈JS代码是如何执行的。读这部分的前提是已经知道了JS引擎是单线程,JS引擎线程
事件触发线程
定时触发器线程
JS分为同步任务和异步任务
同步任务都在主线程上执行,形成一个
执行栈
主线程之外,事件触发线程管理着一个
任务队列
,只要异步任务有了运行结果,就在任务队列
之中放置一个事件。一旦
执行栈
中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列
,将可运行的异步任务添加到可执行栈中,开始执行。
看到这里,应该就可以理解了:为什么有时候setTimeout推入的事件不能准时执行?因为可能在它推入到事件列表时,主线程还不空闲,正在执行其它代码, 所以自然有误差。
六、导航到另一个站点
简单导航已经完毕!但是用户在地址栏输入另一个 URL 会怎样呢?好吧,浏览器进程会执行相同的步骤来导航到一个不同的站点。但是在它做这个之前,它会检查当前已经渲染的站点是否关心 beforeunload
事件。
beforeunload
可以在你试图导航离开或关闭标签页时创建“离开此站点?”警告。包括你的 JavaScript 代码,所有标签页内的东西都是由渲染进程处理,所以当新的导航请求到来时,浏览器进程必须要跟当前的渲染进程核对。
注意: 不要添加无条件的 beforeunload
处理程序。它会产生更多延迟,因为处理程序需要在导航开始之前执行。应仅在需要时添加此事件处理程序,例如如果需要警告用户他们可能会丢失他们在页面上输入的数据。
[图:浏览器进程向渲染进程发送 IPC 告诉它将要导航到另一个站点]
如果渲染进程已经启动了导航(像用户点击一个链接或者客户端 JavaScript 运行 window.location = "https://newsite.com"
),渲染进程会先检查 beforeunload
事件处理程序。然后,它会像浏览器处理启动导航一样执行相同的步骤。唯一不同的是导航请求是由渲染进程发送到浏览器进程的。
当新导航到的站点不同于当前已渲染的站点时,会调用一个独立的渲染进程来处理新导航,同时保持当前的渲染进程来处理类似 unload
的事件。有关更多信息,请查看页面生命周期概览以及如何使用页面声明周期 API 挂钩事件。
[图:2 个 IPC(从浏览器进程到新渲染进程)告知渲染页面并告知旧渲染进程卸载]
七、如果有 Service Worker
最近对导航过程的改变是引入了 Service Worker。Service Worker 是一种在你的应用代码中编写网络代理的方法;允许 Web 开发者更好地控制本地缓存内容以及何时从网络获取新数据。如果将 Service Worker 设置为从缓存加载页面,则无需从网络请求数据。
要记住的重要部分是 Service Worker 是在渲染进程中运行的 JavaScript 代码。但是当导航请求进入时,浏览器进程如何知道该站点有 Service Worker?
[图:浏览器进程中的网络线程查找 Service Worker 作用域]
当注册一个 Service Worker 时,保持 Service Worker 的作用域作为一个引用(你可以在这篇文章 The Service Worker Lifecycle 中阅读更多关于作用域的知识)。当一个导航发生时,网络线程用已注册的 Service Worker 作用域来检查域名,如果已经为该 URL 注册了一个 Service Worker,GUI 线程会找一个渲染线程来执行 Service Worker 的代码。Service Worker 可能从缓存中加载数据,无需从网络请求数据,或者可以从网络请求新资源。
[图:浏览器中的GUI 线程启动渲染进程来处理Service Workers。然后,渲染进程中的工作线程从网络请求数据]
八、导航预加载
你可以看到,如果 Service Worker 最终决定从网络请求数据,则浏览器进程和渲染器进程之间的往返可能会导致延迟。导航预加载是一种通过与 Service Worker 启动并行加载资源来加速此过程的机制。它用一个头部来标记这些请求,允许服务器决定为这些请求发送不同的内容;例如,只更新数据而不是完整文档。
[图:浏览器进程中的 GUI 线程启动渲染进程以在并行启动网络请求的同时处理 Service Worker]