前言:
根据上次的Vue.$nextTick() 引出来的知识点 进行一个对浏览器有关的知识网记录一下。
关键词:浏览器进程、浏览器线程、浏览器内核、浏览器引擎、js运行机制、事件循环、任务队列、宏任务、微任务、同步、异步等。
从一道腾讯的面试题入手:Chrome 浏览器是多进程还是单进程,是多线程还是单线程?
直接上图:
解释一下 浏览器多进程、浏览器内核多线程 (又叫浏览器渲染进程、渲染引擎 )、 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个进程;
3.渲染引擎(渲染进程、浏览器内核)
即图中的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树
- 地址栏输入URL,WebKit调用资源加载器加载相应资源;
- 加载器依赖网络模块建立连接,发送请求并接收答复;
- WebKit接收各种网页或者资源数据,其中某些资源可能同步或异步获取;
- 网页交给HTML解析器转变为词语;
- 解释器根据词语构建节点,形成DOM树;
- 如果节点是JavaScript代码,调用JavaScript引擎解释并执行;
- JavaScript代码可能会修改DOM树结构;
- 如果节点依赖其他资源,如图片\css、视频等,调用资源加载器加载它们,但这些是异步加载的,不会阻碍当前DOM树继续创建;如果是JavaScript资源URL(没有标记异步方式),则需要停止当前DOM树创建,直到JavaScript加载并被JavaScript引擎执行后才继续DOM树的创建。
3.4.2从DOM树到构建WebKit绘图上下文
- CSS文件被CSS解释器解释成内部表示;
- CSS解释器完成工作后,在DOM树上附加样式信息,生成RenderObject树;
- RenderObject节点在创建的同时,WebKit会根据网页层次结构构建RenderLayer树,同时构建一个虚拟绘图上下文。
3.4.2.绘图上下文到最终图像呈现
- 绘图上下文是一个与平台无关的抽象类,它将每个绘图操作桥接到不同的具体实现类,也就是绘图具体实现类;
- 绘图实现类也可能有简单的实现,也可能有复杂的实现,软件渲染、硬件渲染、合成渲染等;
- 绘图实现类将2D图形库或者3D图形库绘制结果保存,交给浏览器界面进行展示。
上述是一个完整的渲染过程,现代网页很多都是动态的,随着网页与用户的交互,浏览器需要不断的重复渲染过程。
3.4.3 简写目录:
- DNS 解析成 IP 地址
- 发送 http 请求
- TCP 传输报文
- IP 寻址
- 封装成帧
- 物理传输
- 页面渲染主流程
- dom树和render树的关系
- 布局render树(layout)
- 绘制(paint)
这里的页面渲染就是 浏览器渲染引擎的工作了,我们简单记录一下这个步骤 详情可以看这个:
4.页面渲染
渲染引擎首先通过网络获得所请求文档的内容,通常以8K分块的方式完成。下面是渲染引擎在取得内容之后的基本流程:
解析html以构建dom树 -> 构建render树 -> 布局render树 -> 绘制render树
- 解析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引擎线程把握的,有的是定时触发器线程把握的就比如定时器任务。
这个经典图来自一个经典的讲解事件循环的视频:事件循环讲堂
然后 可视化事件循环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) 有setTimeout、MessageChannel、postMessage、setImmediate; - 微任务(
micro task)有MutationObsever和Promise.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语法树等等。
参考资料: