CPU、进程、线程
进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)线程是cpu调度的最小单位(线程是创建在进程的基础上的一次程序运行单位,一个进程中能够有多个线程)- 不同
进程之间也能够通讯,不过代价较大 单线程与多线程,都是指在一个进程内的单和多
对于计算机来讲,每个应用程序都是一个进程, 而每个应用程序都会分别有不少的功能模块,这些功能模块其实是经过子进程来实现的。 对于这种子进程的扩展方式,咱们能够称这个应用程序是多进程的。
对于浏览器来讲,浏览器就是多进程的,比如在Chrome浏览器中打开了多个tab,每个Tab页,就是一个独立的进程。
浏览器架构
对于浏览器,可以使一个进程有许多不同的线程,也可以是许多不同的进程有多个线程通过 IPC 通信的。
而对于Chrome浏览器,最新架构如下图:
浏览器包含了哪些进程
| 进程 | 作用 |
|---|---|
| Browser | 主进程(浏览器进程),控制应用程序的“chrome”部分,包括地址栏、书签、后退和前进按钮。还处理 Web 浏览器的不可见的特权部分,例如网络请求和文件访问。 |
| Renderer | 渲染器进程,控制显示网站的选项卡内的任何内容。就是咱们说的浏览器内核,每一个tab页一个渲染进程 |
| Plugin | 插件进程,控制网站使用的任何插件,例如 flash。 |
| GPU | 图形处理进程,独立于其他进程处理 GPU 任务。它被分成不同的进程,因为 GPU 处理来自多个应用程序的请求并将它们绘制在同一个表面上。 |
下图为不同进程指向浏览器UI的不同部分:
浏览器内核(渲染进程)包含了哪些线程
| 线程 | 作用 |
|---|---|
| GUI渲染线程 | - 负责渲染页面,布局和绘制 - 页面须要 重绘和回流时,该线程就会执行 - 与js引擎线程互斥,防止渲染结果不可预期 |
| JS引擎线程 | - 负责处理解析和执行javascript脚本程序 - 只有一个JS引擎线程(单线程) - 与GUI渲染线程互斥,防止渲染结果不可预期 |
| 事件触发线程 | - 用来控制事件循环(鼠标点击、setTimeout、ajax等) - 当事件知足触发条件时,将事件放入到JS引擎所在的执行队列中 |
| 定时触发器线程 | - setInterval与setTimeout所在的线程- 定时任务并非由JS引擎计时的,是由定时触发线程来计时的 - 计时完毕后,通知事件触发线程 |
| 异步http请求线程 | - 浏览器有一个单独的线程用于处理AJAX请求 - 当请求完成时,如有回调函数,通知事件触发线程 |
为何 javascript 是单线程的
Javascript 作为一门浏览器端的脚本语言,主要的任务就是处理用户的交互,而用户的交互无非就是响应 DOM 上的一些事件/增删改 DOM 中的元素。如果 Javascript 被设计为多线程的程序,那么操作 DOM 必然会涉及到资源的竞争,引发不可预期的结果。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JS脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JS单线程的本质。
为何 GUI 渲染线程与 JS 引擎线程互斥
JS 是能够操做 DOM 的,若是同时修改元素属性并同时渲染界面(即 JS线程和GUI线程同时运行), 那么渲染线程先后得到的元素就可能不一致了。
JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了可以使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工做,对页面进行渲染。
宏任务-->渲染-->宏任务-->渲染-->渲染...
Event Loop 与 JS 的运行机制
当代码执行到setTimeout/setInterval时,其实是JS引擎线程通知定时触发器线程,间隔一个时间后,会触发一个回调事件, 而定时触发器线程在接收到这个消息后,会在等待的时间后,将回调事件放入到由事件触发线程所管理的事件队列中。
当代码执行到XHR/fetch时,其实是JS引擎线程通知异步http请求线程,发送一个网络请求,并制定请求完成后的回调事件, 而异步http请求线程在接收到这个消息后,会在请求成功后,将回调事件放入到由事件触发线程所管理的事件队列中。
当咱们的同步任务执行完,JS引擎线程会询问事件触发线程,在事件队列中是否有待执行的回调函数,若是有就会加入到执行栈中交给JS引擎线程执行
输入url到加载完页面发生了什么
一、导航跳转
| 步骤 | 作用 | 图 |
|---|---|---|
| step1. 处理输入 | 浏览器中的地址栏输入内容时,Browser Process中的UI线程会进行解析,判定所输内容是搜索查询还是 URL后,再将其发送到搜索引擎,还是发送到请求的站点。 | |
| step2. 开始寻找 | 地址栏输入内容按下回车键后,Browser Process中的网络线程会进行DNS查找,并建立TLS连接。如果收到服务器重定向标头(如HTTP301),网络线程会与服务器请求重定向的UI线程通信,然后发起另一个URL请求。 | |
| step3. 读取响应 | 当请求响应返回的时候,网络线程会依据 Content-Type 判定数据类型,完成MIME Type sniffing以判断数据是否丢失或错误 | |
如果响应内容的格式是 HTML ,下一步将会把这些数据传递给renderer process,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。也正是在这个地方进行安全浏览检查,如果域和相应数据跟恶意网站匹配, 网络线程就会发出警报并显示警告页面,此时CORS检查也会触发确保敏感数据不会被传递给渲染进程 | ||
| step4. 查找渲染器进程 | 当上述所有检查完成,network thread 确信浏览器可以导航到请求网页,network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。由于网络请求获取响应需要时间,这里其实还存在着一个加速方案。当 UI thread 发送 URL 请求给 network thread 时,浏览器其实已经知道了将要导航到那个站点。UI thread 会并行的预先查找和启动一个渲染进程,如果一切正常,当 network thread 接收到数据时,渲染进程已经准备就绪了,但是如果遇到重定向,准备好的渲染进程也许就不可用了,这时候就需要重启一个新的渲染进程。 | |
| step5. 提交 | 此时数据以及渲染进程已准备就绪, Browser Process 会跟 renderer process 进行IPC通信,请求renderer process渲染页面。一旦 Browser Process 收到 renderer process 的渲染确认消息,导航过程结束,页面加载过程开始。此时,地址栏会更新,展示出新页面的网页信息。history tab 会更新,可通过返回键返回导航来的页面,为了让关闭 tab 或者窗口后便于恢复,这些信息会存放在硬盘中。 | |
| 其他步骤:首帧渲染完成 | 当 renderer process 渲染结束(渲染结束意味着该页面内的所有的页面,包括所有 iframe 都触发了 onload 时),会发送 IPC 信号到 Browser process, UI thread 会停止展示 tab 中的 spinner。此时网页只是首帧渲染完成,在此之后,客户端依旧可下载额外的资源渲染出新的视图。 |
如果用户再次将不同的 URL 放入地址栏,导航到其他站点会发生什么?
浏览器进程通过相同的步骤导航到不同的站点。但在此之前,它需要检查当前呈现的站点是否有 beforeunload 事件。
beforeunload 可以创建一个 “离开此站点?” 的事件, 当离开或关闭选项卡时发出警报。选项卡内的所有内容(包括 JavaScript 代码)都由渲染器进程处理,因此当新的导航请求传入时,浏览器进程必须检查当前的渲染器进程。
注意:不要添加无条件
beforeunload处理程序。它会产生更多的延迟,因为需要在导航开始之前执行处理程序。仅在需要时才应添加此事件处理程序,例如,如果需要警告用户他们可能会丢失在页面上输入的数据。
如果导航到新的网站,会启用一个新的 render process 来处理新页面的渲染,老的进程会留下来处理类似 unload 等事件。
下图为从浏览器进程到新渲染器进程的 2 个 IPC,告诉渲染页面并告诉旧渲染器进程卸载(更多页面生命周期):
Service Worker
Service Worker 允许开发者更好地控制本地缓存的内容以及何时从网络获取新数据。如果 service worker 设置为从缓存加载页面,则无需从网络请求数据。
注意:Service Worker 是在渲染器进程中运行的 JavaScript 代码。
但是当导航请求进来时,浏览器进程又如何知道哪个站点有Service Worker?
注册Service Worker后,Service Worker的作用域将会保留。当导航发生时,网络线程会根据注册的 Service Worker 范围检查域,如果 Service Worker 已为该 URL 注册,则 UI 线程会查找渲染器进程以执行 Service Worker 代码。Service Worker 可能会从缓存中加载数据,从而无需从网络请求数据,或者它可能会从网络请求新资源。
下图为浏览器进程中的 UI 线程启动渲染器进程来处理服务工作者;渲染器进程中的工作线程然后从网络请求数据:
导航预加载
如果 Service Worker 最终决定从网络请求数据,浏览器进程和渲染器进程之间的这种往返可能会导致延迟。Navigation Preloads 是一种通过在 Service Worker 启动的同时加载资源来加速此过程的机制。它用标头标记这些请求,允许服务器决定为这些请求发送不同的内容;例如,只是更新数据而不是完整文档。
二、渲染
导航过后,浏览器会调用渲染器(UI)进程工作。Renderer process几乎负责 Tab 内的所有事情,核心工作是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页。它主要包含以下线程:
- 主线程 Main thread
- 工作线程 Worker thread
- 排版线程 Compositor thread
- 光栅线程 Raster thread
1. 构建 DOM
当渲染进程接收到导航的确认信息,开始接受 HTML 数据时,主线程会解析文本字符串为 DOM。渲染 html 为 DOM 的方法由 HTML Standard 定义。
2. 子资源加载
网页中的图片、CSS、JS 等外部资源,通常需要从网络上或者 cache 中获取。主进程可以在构建 DOM 的过程中会逐一请求它们,为了加速 preload scanner会同时运行,如果在 html 中存在 <img> <link> 等标签,preload scanner会把这些请求传递给 Browser process 中的 network thread 进行相关资源的下载。
3. JS 的下载与执行
当碰到HTML中的<script>标签时,会暂停 HTML 文档的解析,先去加载、解析和执行 JavaScript 代码。
这样设计的原因是JS 可能会改变 DOM 的结构(使用诸如 document.write()等API)。
如果JavaScript 不使用document.write(),可以添加 async 或 defer 属性到<script>标签。然后浏览器异步加载和运行 JavaScript 代码,并且不会阻止解析。浏览器支持的话,当然也可以用 Javascript Module。<link rel="preload">是一种通知浏览器当前导航肯定需要该资源并且希望尽快下载的方式。查看更多:Resource Prioritization – Getting the Browser to Help You
4. 样式计算
仅仅渲染 DOM 还不足以获知页面的具体样式,主进程还会基于 CSS 选择器解析 CSS 获取每一个节点的最终的计算样式值。即使不提供任何 CSS,浏览器对每个元素也会有一个默认的样式。默认样式表
5. 布局
该过程是获知每一个节点在页面上的位置,主线程遍历 DOM 和计算样式并创建布局树,其中包含 xy 坐标和边界框大小等信息。
如果 display: none 应用,则该元素不是布局树的一部分(但是,具有的 visibility: hidden 在布局树中)。类似地,如果应用了具有类似内容的伪类,p::before{content:"Hi!"} 即使它不在 DOM 中,它也会包含在布局树中。
6. 绘制
到目前为止,有了DOM、样式和布局,但是想要开始绘制需要判断绘制的顺序。例如,z-index可能会为某些元素设置,在这种情况下,按照 HTML 中编写的元素的顺序绘制将导致不正确的渲染。
在绘制步骤中,主线程遍历布局树以创建绘制记录。绘制记录的顺序是:先背景,后文字,再矩形。这个和 <canvas> 的绘制过程有点像。
注意:绘制过程最重要的一点是:绘制的每一步都使用前一操作的结果来创建新数据。如果布局树中的某些内容发生了变化,则需要为文档的受影响部分重新生成绘制顺序。
7. 合成帧
三、浏览器对事件的处理
1. 浏览器的input事件
在浏览器的看来,用户的所有手势都是输入,鼠标滚动,悬置,点击等等都是。
当用户在屏幕上触发诸如 touch 等手势时,首先收到手势信息的是 Browser process, 不过 Browser process 只会感知到在哪里发生了手势,对 tab 内内容的处理是还是由渲染进程控制的。
事件发生时,浏览器进程会将事件类型(如touchstart)及其坐标发送给渲染进程,渲染进程通过查找事件目标并运行附加的事件侦听器来处理事件。
下图为Input事件通过浏览器进程路由到渲染器进程:
2. 非快速滚动区域
如果没有input事件监听器附加到页面,合成器线程可以创建一个完全独立于主线程的新复合框架,如果某些事件侦听器附加到页面上,合成器线程如何确定事件是否需要处理?
由于运行 JavaScript 是主线程的工作,因此在合成页面时,合成器线程会将页面中附加有事件处理程序的区域标记为“非快速可滚动区域”。通过获得这些信息,合成器线程可以确保在该区域发生事件时将input事件发送到主线程。如果input事件来自该区域之外,则合成器线程继续合成新帧,而无需等待主线程。
3. 事件委托
开发中常见的事件处理模式是事件委托。由于事件冒泡,可以在最顶层元素附加一个事件处理程序,并根据事件目标委派任务,比如:
document.body.addEventListener('touchstart',
event => {
if (event.target === area) {
event.preventDefault();
}
}
);
如果需要为所有元素编写一个事件处理程序的话,这种事件委托模式很有吸引力。但是,如果从浏览器的角度来看这段代码,现在整个页面都被标记为非快速可滚动区域。这意味着即使程序不关心来自页面某些部分的输入,合成器线程也必须与主线程通信并在每次输入事件进入时等待它。因此,流畅的合成器独立处理合成帧的模式就失效了。
为了防止这种情况,我们可以为事件处理器传递 passive: true 做为参数,这样写就能让浏览器即监听相关事件,又让组合器线程在等等主线程响应前构建新的组合帧。
document.body.addEventListener('touchstart',
event => {
if (event.target === area) {
event.preventDefault()
}
}, {passive: true}
);
4.检查事件是否可以取消
有个场景,只有水平滚动,没有垂直滚动。
上述写法可能又会带来另外一个问题,假设某个区域你只想要水平滚动,使用 passive: true 可以实现平滑滚动,但是垂直方向的滚动可能会先于event.preventDefault()发生,此时可以通过 event.cancelable 来防止这种情况。
document.body.addEventListener('pointermove', event => {
if (event.cancelable) {
event.preventDefault(); // block the native scroll
/*
* do what you want the application to do here
*/
}
}, {passive: true});
也可以使用css属性 touch-action 来完全消除事件处理器的影响,如:
#area {
touch-action: pan-x;
}
5.寻找事件对象
当合成器线程向主线程发送输入事件时,首先要运行的是命中以找到事件目标。命中使用渲染过程中生成的绘制记录数据来找出发生事件的点坐标下方的内容。
6.事件优化:最小化事件调度到主线程
前面知道了,典型显示器每秒刷新屏幕 60 次,以及我们需要跟上节奏以获得流畅的动画。而对于输入来说。典型的触摸屏设备每秒传递 60-120 次触摸事件,典型的鼠标每秒传递 100 次事件。输入事件的保真度高于我们的屏幕可以刷新的保真度。
如果像touchmove这样的连续事件每秒发送到主线程 120 次,那么与屏幕刷新的速度相比,它可能会触发过多的命中和 JavaScript 执行:
为了尽量减少对主线程的过多调用,Chrome 会合并连续事件(例如 wheel, mousewheel, mousemove, pointermove, touchmove)并延迟调度直到下一个requestAnimationFrame,可以发现,时间线一样,但事件进行了合并和延迟。
而keydown,keyup,mouseup,mousedown,touchstart,和touchend 这些非连续性事件会被立即执行。
7. 使用 getCoalescedEvents 得到帧内事件
合并事件虽然能提示性能,但是如果你的应用是绘画等,则很难绘制一条平滑的曲线了,此时可以使用 getCoalescedEvents API 来获取合并事件的信息。示例代码如下:
window.addEventListener('pointermove', event => {
const events = event.getCoalescedEvents();
for (let event of events) {
const x = event.pageX;
const y = event.pageY;
// draw a line using x and y coordinates.
}
});
下图左侧是平滑的触摸手势路径,右侧是合并的有限路径:
相关链接: