进程和线程
进程(process)和线程(thread)是操作系统的基本概念。
现代操作系统都是可以同时运行多个任务的,比如:用浏览器上网的同时还可以听音乐。对于操作系统来说,一个任务就是一个进程,比如打开一个浏览器就是启动了一个浏览器进程,打开一个 Word 就启动了一个 Word 进程。
有些进程同时不止做一件事,比如 Word,它同时可以进行打字、拼写检查、打印等事情。在一个进程内部,要同时做多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程。
由于每个进程至少要做一件事,所以一个进程至少有一个线程。
系统会给每个进程分配独立的内存,因此进程有它独立的资源。同一进程内的各个线程之间共享该进程的内存空间(包括代码段,数据集,堆等)。
如果电脑是 windows 系统,打开任务管理器,可以看到有一个后台进程列表,在这里我们可以看到每个进程的内存资源信息以及 CPU 占有率。
我们再用官方的术语描述一下:
进程是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位)。
线程是 CPU 调度的最小单位(是建立在进程基础上的一次程序运行单位)。
浏览器是多进程的
理解了进程和线程之后,接下来我们对浏览器进行一定程度上的认识。
浏览器是多进程的,每打开一个 tab 页,就相当于创建了一个独立的浏览器进程。
图中打开了 Chrome 浏览器的多个 tab 页,在 Chrome 任务管理器
中可以看到有多个进程,每一个 tab 页有一个独立的进程。
注意:浏览器应该也有自己的优化机制,有时候打开多个 tab 页,在 Chrome 任务管理器中会看到有些进程被合并了,所以每个 tab 页对应一个进程并不一定是绝对的。
浏览器包含哪些进程?
为了简化理解,这里仅列举主要进程。
-
Browser 进程:浏览器的主进程,只有一个。
负责浏览器界面的显示与交互;
各个页面的管理,创建和销毁其他进程;
网络的资源管理、下载等。
Renderer 进程:也称为浏览器渲染进程或浏览器内核,内部是多线程的。主要负责页面渲染,脚本执行,事件处理等。
第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建。
GPU 进程:最多一个,用于 3D 绘制等。
浏览器多进程的优势
由于默认 新开 一个 tab 页面 新建 一个进程,所以单个 tab 页面崩溃不会影响到整个浏览器;
同样,第三方插件崩溃也不会影响到整个浏览器;
多进程可以充分利用现代 CPU 多核的优势;
方便使用沙盒模型隔离插件等进程,提高浏览器的稳定性。
系统为浏览器新开的进程分配内存、CPU 等资源,所以内存和 CPU 的资源消耗也会更大。
浏览器内核(渲染进程)
前面说了这么多的进程,对普通前端操作来说,最重要的还是渲染进程。
浏览器的渲染进程是多线程的,页面的渲染,JS的执行,事件的循环等,都在这个进程内执行。
渲染进程通常由以下常驻线程组成:
1. GUI 渲染线程
负责渲染浏览器界面,解析 HTML、CSS,构建 DOM tree和 render tree,布局和绘制等。当界面需要重绘(repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。
2. JS 引擎线程
也称为 JS 内核,负责解析 JavaScript 脚本,运行代码。
-
JavaScript 是单线程的。
JavaScript 为什么是单线程的?这与它的用途有关。JavaScript 作为浏览器脚本语言,主要用途是与用户互动以及操作 DOM。这也决定了它只能是单线程的,否则会带来很复杂的同步问题。想想一下,如果 JavaScript 同时有连个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个 DOM 节点,这时浏览器应该以哪个线程为准呢?所以,为了避免复杂性,JavaScript 从一开始就是单线程。
-
GUI 渲染线程 与 JS 引擎线程是互斥的。
由于 JavaScript 可以操作 DOM,如果在修改元素属性的同时渲染界面(即 JavaScript 引擎线程和 GUI 渲染线程同时运行),那么渲染线程前后获得的元素数据就可能会不一致。因此,为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JS 引擎为互斥的关系。当 JS 引擎执行时,GUI 线程被挂起,GUI 更新被保存在一个队列中,等到 JS 引擎线程空闲时立即被执行。
-
JS 阻塞页面加载。
由于 GUI 渲染线程与 JS 引擎线程是互斥的,当浏览器在执行 JavaScript 的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。因此如果 JS 执行时间过长,就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
3. 事件触发线程
当一个事件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎处理。这些事件可以是当前执行的代码块,如定时任务;也可以是来自浏览器内核的其他线程,如:鼠标点击、Ajax异步请求等。但由于 JS 是单线程的,这些事件都需要排队等待 JS 引擎处理。
4. 定时触发器线程
setTimeout
和 setInterval
所在的线程。浏览器定时计数器并不是由 JS 引擎计数的,因为 JS 是单线程的,如果处于阻塞线程状态就会影响计时的准确,所以通过单独的线程来计时并触发定时更为合理。
5. 异步 http 请求线程
XMLHttpRequest 在建立连接后,通过浏览器新开一个线程请求,一旦检测到状态变更并且设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中,等待 JS 引擎空闲时处理。
Browser 进程和 Renderer 进程的通信过程
打开浏览器的一个 tab 页时,我们看下其中的大致过程:
Browser 进程收到用户请求,通过网络下载获取页面内容,然后将该任务通过RendererHost接口传递给 Renderer 进程;
-
Renderer 进程的 Renderer 接口收到消息,简单解释后,交给 GUI 渲染线程开始渲染;
GUI 渲染线程接收请求,加载网页并渲染网页,这个过程中可能需要 Browser 进程获取资源和 GPU 进程来帮助渲染,也可能会有 JS 引擎线程操作 DOM(可能造成回流并重绘);
最后 Renderer 进程将结果传递给 Browser 进程;
Browser 进程接收到结果,并将结果绘制出来。
到这里应该对浏览器的运作有一定理解了,我们再来看下浏览器是怎么渲染页面的。
浏览器的渲染流程
浏览器内核拿到页面内容后,渲染过程大概分为以下几个部分:
- 解析 HTML 文件,生成 DOM tree;同时解析 CSS 文件以及样式元素中的样式数据,生成 CSS Rules。
- 构建 render tree:根据 DOM tree 和 CSS Rules 来构建 render tree,它可以让浏览器按照正确的顺序绘制内容。
- 布局(layout / reflow):计算各元素尺寸、位置。
- 绘制(paint):绘制页面像素信息。
- 浏览器将各层信息发送给 GPU,GPU 将各层信息合成(composite),显示在屏幕上。
补充:
Webkit 将 render tree 中的元素称为 render object (或 renderer),每一个 render object 都代表一个的矩形区域,通常对应于相关节点的 CSS 框,这些矩形的排列顺序就是它们在屏幕上显示的顺序。
Render object 和 DOM 节点是相对应的,但并非一一对应。非可视化的 DOM 元素不会插入 render tree 中,例如“head”元素 和 一些 display: none 的节点就没必要放在 render tree 中了。
这里只是大致的过程,详细步骤可以看参考资料中的第一篇。
渲染完成后,接下来就是 JavaScript 逻辑处理了。