浏览器进程和线程知识网搭建

110 阅读1分钟

前言:

根据上次的Vue.$nextTick() 引出来的知识点 进行一个对浏览器有关的知识网记录一下。

关键词:浏览器进程、浏览器线程、浏览器内核、浏览器引擎、js运行机制、事件循环、任务队列、宏任务、微任务、同步、异步等。

从一道腾讯的面试题入手:Chrome 浏览器是多进程还是单进程,是多线程还是单线程?

直接上图:

5.png 解释一下 浏览器多进程浏览器内核多线程 (又叫浏览器渲染进程、渲染引擎 )、 JS引擎单线程GUI渲染线程和JS引擎线程互斥

1.进程和线程

1.1含义:

进程和线程都是操作系统的基本概念,cpu工作时间段的描述。

进程:是cpu资源分配的最小单位。(能拥有资源和独立运行的最小单位)

线程:是cpu进行运算调度的最小单位。(是进程的一个执行流)

1.2之间关系:

操作系统引入进程的概念,从理论角度看,是对正在运行的程序过程的抽象;从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序

不同进程之间可以通信(进程通信管道IPC), 代价较大,所以就出现了线程的概念。

线程之间(是指一个进程内的线程,即单线程、多线程)共享进程的所有资源,每个线程有自己的堆栈和局部变量。

线程由 CPU 独立调度执行,在多 CPU 环境下就允许多个线程同时运行。

同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

参考一下 阮一峰的工厂比喻:进程与线程的一个简单解释

2.浏览器的多进程

07年之前的浏览器是单进程的,一个进程负责网络、JS运行环境、渲染引擎、页面、插件等,导致不稳定,一个线程卡死,整个 程序就会出现问题。

现在以Chrome浏览器为例,是多进程的,打开一个tab页就相当于多开了一个进程,进程与进程之间完全不影响。 安全,稳定,流畅。

打开一个浏览器页面:1个浏览器进程、1个渲染进程、1个GPU进程、1个网络进程,一共4个进程

5.png

3.渲染引擎(渲染进程、浏览器内核)

6.png

即图中的Render进程:

GUI渲染线程
  • 负责渲染页面,布局和绘制
  • 页面需要重绘和回流时,该线程就会执行
  • 与js引擎线程互斥,防止渲染结果不可预期
JS引擎线程
  • 负责处理解析和执行javascript脚本程序
  • 只有一个JS引擎线程(单线程)
  • 与GUI渲染线程互斥,防止渲染结果不可预期
事件触发线程
  • 用来控制事件循环(鼠标点击、setTimeout、ajax等)
  • 当处理一些不能立即执行的代码时,会将对应的任务在其可以触发的时机,添加到事件队列的末端
  • 事件循环机制会在JS引擎线程空闲时,循环访问事件队列的头部,如果有函数,则会将该函数推到执行栈中并立即执行
定时触发器线程
  • setInterval与setTimeout所在的线程
  • 定时任务并不是由JS引擎计时的,是由定时触发线程来计时的
  • 计时完毕后,将回调事件放入到事件队列 中等待 JS 引擎处理。
异步http请求线程
  • 浏览器有一个单独的线程用于处理AJAX请求
  • 当请求完成时,若有回调函数,将回调事件放入到事件队列中 等待 JS 引擎处理。
3.1GUI渲染线程与js引擎线程互斥

由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系,当 JavaScript 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到引擎线程空闲时立即被执行。

3.2 JS 阻塞页面加载

GUI 渲染线程与 JavaScript 执行线程是互斥 。当浏览器在执行 JavaScript 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。因此如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

3.3 进程之间的通信

打开浏览器的一个 tab 页时,我们看下其中的大致过程:

  • Browser 进程收到用户请求,通过网络下载获取页面内容,然后将该任务通过 RendererHost 接口传递给 Renderer 进程;
  • Renderer 进程的 Renderer 接口收到消息,简单解释后,交给 GUI 渲染线程开始渲染;
  • GUI 渲染线程接收请求,加载网页并渲染网页,这个过程中可能需要 Browser 进程获取资源和 GPU 进程来帮助渲染,也可能会有 JS 引擎线程操作 DOM(可能造成回流并重绘);
  • 最后 Renderer 进程将结果传递给 Browser 进程;
  • Browser 进程接收到结果,并将结果绘制出来。

这里又延伸一个面试题:从url地址栏输入网址 到浏览器页面渲染成功 之间发生了什么事情。

3.4 输入网址 到浏览器页面渲染成功

从输入baidu.com 到baidu首页完全展现这个过程可以大致分为 网络通信页面渲染 两个步骤

3.4.1从输入URL到生成DOM树

  1. 地址栏输入URL,WebKit调用资源加载器加载相应资源;
  2. 加载器依赖网络模块建立连接,发送请求并接收答复;
  3. WebKit接收各种网页或者资源数据,其中某些资源可能同步或异步获取;
  4. 网页交给HTML解析器转变为词语;
  5. 解释器根据词语构建节点,形成DOM树;
  6. 如果节点是JavaScript代码,调用JavaScript引擎解释并执行;
  7. JavaScript代码可能会修改DOM树结构;
  8. 如果节点依赖其他资源,如图片\css、视频等,调用资源加载器加载它们,但这些是异步加载的,不会阻碍当前DOM树继续创建;如果是JavaScript资源URL(没有标记异步方式),则需要停止当前DOM树创建,直到JavaScript加载并被JavaScript引擎执行后才继续DOM树的创建。

3.4.2从DOM树到构建WebKit绘图上下文

  1. CSS文件被CSS解释器解释成内部表示;
  2. CSS解释器完成工作后,在DOM树上附加样式信息,生成RenderObject树;
  3. RenderObject节点在创建的同时,WebKit会根据网页层次结构构建RenderLayer树,同时构建一个虚拟绘图上下文。

3.4.2.绘图上下文到最终图像呈现

  1. 绘图上下文是一个与平台无关的抽象类,它将每个绘图操作桥接到不同的具体实现类,也就是绘图具体实现类;
  2. 绘图实现类也可能有简单的实现,也可能有复杂的实现,软件渲染、硬件渲染、合成渲染等;
  3. 绘图实现类将2D图形库或者3D图形库绘制结果保存,交给浏览器界面进行展示。

上述是一个完整的渲染过程,现代网页很多都是动态的,随着网页与用户的交互,浏览器需要不断的重复渲染过程。

3.4.3 简写目录:

  • DNS 解析成 IP 地址
  • 发送 http 请求
  • TCP 传输报文
  • IP 寻址
  • 封装成帧
  • 物理传输
  • 页面渲染主流程
  • dom树和render树的关系
  • 布局render树(layout)
  • 绘制(paint)

这里的页面渲染就是 浏览器渲染引擎的工作了,我们简单记录一下这个步骤 详情可以看这个:

从输入cnblogs.com到博客园首页完全展示发生了什么

4.页面渲染

渲染引擎首先通过网络获得所请求文档的内容,通常以8K分块的方式完成。下面是渲染引擎在取得内容之后的基本流程:

  解析html以构建dom树 -> 构建render树 -> 布局render树 -> 绘制render树

7.png

  • 解析html建立dom树
  • 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
  • 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
  • 绘制render树(paint),绘制页面像素信息
  • 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上

5.JS引擎的一些运行机制分析

5.1JS引擎是单线程

JavaScript本质上是一种解释型语言,它需要一遍执行一边解析,而编译型语言在执行时已经完成编译,可直接执行,有更快的执行速度。JavaScript代码是在浏览器端解析和执行的,如果需要时间太长,会影响用户体验。那么提高JavaScript的解析速度就是当务之急。

JS引擎是单线程,由于javascript最初作为浏览器脚本语言,主要用来与用户互动、操作dom等,如果有多个线程同时操作一个DOM的情况,会导致非常难以处理,所以javascript只能设计成单线程。

不过现代计算机基本都是多核CPU的,纯粹的单线程会导致一些性能得不到释放,所以新的 HTML5 标准中提出了 web worker概念,允许用户额外开启线程,不过 worker 线程是完全受主线程控制(大部分情况处理一些计算逻辑),且没有操作DOM的权限,本质上javascript还是单线程

5.2异步任务

javascript只有一个主线程用来执行任务,但是同一时间只能执行一个任务也就是函数,普通的函数会形成一个任务队列排队执行,但是有些任务会非常耗时且不可控(网络请求、事件监听)等,如果让这些任务也和普通任务一样排队执行,那么执行效率低不说还会导致页面的卡死。

于是就有了异步任务,而 V8 引擎 通过消息队列事件循环 系统让异步任务执行且不用排队等待执行完毕。

  • JS分为同步任务和异步任务
  • 同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,事件触发线程管理着一个任务队列(消息队列),只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行
5.3事件循环机制
  • 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

  • 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

  • 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

  • 主线程不断重复上面的第三步。

这里的执行栈就是在主线程JS引擎把握(管理)的,任务队列 就是异步任务要去的地方,事件触发线程把握的。在合适的时间,异步任务状态改变有了结果,就把它放到任务对列里,主任务完成之后 就读取任务对列里的任务,进入执行栈执行,这里的异步任务有的是js引擎线程把握的,有的是定时触发器线程把握的就比如定时器任务。

1-v8.png

2-v8.png 这个经典图来自一个经典的讲解事件循环的视频:事件循环讲堂

然后 可视化事件循环demo地址:事件循环可视化demo

5.4宏任务与微任务 macrotask与microtask

5.4.1宏任务:

  • 每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
  • 每一个task会从头到尾将这个任务执行完毕,不会执行其它
  • 浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->...
  • 并且每执行完一个个宏任务(macro task)后,都要去清空该宏任务所对应的微任务队列中所有的微任务(micro task

5.4.2微任务:

当前 task 执行结束后立即执行的任务,当前task任务后,下一个task之前,在渲染之前。所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染

5.4.3 在浏览器环境中,常见的

  • 宏任务(macro task) 有 setTimeoutMessageChannelpostMessagesetImmediate
  • 微任务(micro task)有MutationObseverPromise.then。 process.nextTick(node环境下的微任务)

上篇文章讲了 vue的源码Vue.$nextTick()就是用了MutationObsever setImmediate Promise.then setTimeout v2.5+之后MessageChannel替换了MutationObsever

5.4.4根据线程来理解下:

  • macrotask中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护
  • microtask中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前macrotask执行完毕后执行,而这个队列由JS引擎线程维护 (因为它是在主线程下无缝执行的)

5.4.5运行机制:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

从在浏览器进程和线程的角度,了解事件循环机制以及渲染进程下的各个线程。

这是在浏览器进程线程的知识背景下,掌握事件循环和渲染情况,更多的详细细节还需要自己逐个的去补充,比如:浏览器从url输入到页面渲染,里面的网络https知识、浏览器v8引擎、GUI渲染详细过程、解析器和AST语法树等等。

参考资料:

浏览器进程与线程

浏览器进程和线程

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

认识 V8 引擎

V8引擎详解(八)——消息队列