浏览器的进程、线程、Web Worker及Event Loop

一些计算机概念

CPU

中央处理器,负责运算和控制。


  • 物理线程。多核CPU才能支持线程并行,否则只能并发。
  • 逻辑CPU
    通过超线程技术,单个核可以支持多个逻辑处理器(通常为1~2),每个逻辑处理器可以并行执行一个线程。
  • vCPU
    虚拟处理器,即虚拟机内可以并行的线程数。等于CPU数×核数×超线程数。
GPU

图形处理器,更擅长利用多核心同时处理单一的任务,在图像处理方面有优势


进程和线程

  • 操作系统(OS)会为进程分配cpu和内存,进程是最小的资源分配单位
  • 每个进程至少包含一个线程,线程是资源调度的基本单位
  • 同一进程的线程之间共享进程中的资源(数据、内存等)。
    不同进程之间数据通常相互隔离,如果需要通信,则需要使用IPC(Inter-Process Communication)技术
  • 当一个进程关闭之后,操作系统会回收进程所占用的内存(包括因操作不当导致的内存泄漏)

串行 并行 并发 协程

  • 串行 多个任务,执行时一个执行完再执行另一个。
  • 并行 多个任务同时执行。
  • 并发 一个CPU同时只能执行一个进程,其多个核心可以分别执行一个线程。系统不停切换线程,看起来像同时运行
  • 协程 进程和线程占用内存大,且需通过CPU调度切换,切换过程耗时长。而协程并不增加线程数,切换代价小。

1、单CPU中进程只能是并发,多CPU中进程可以并行。
2、单CPU单核中线程只能并发,单CPU多核中线程可以并行。


浏览器是多进程的

通过Chrome的更多工具 -> 任务管理器 可以查看进程信息
通常每个网页的渲染进程(Renderer)和每种第三方插件占用一个进程,所有网页公用一个Browser主进程(前进、后退、下载等)和GPU绘图进程

Chrome=>更多工具=>任务管理器

网页的渲染进程(Render)

1. GUI渲染线程

又称 CRP关键渲染路径 Critical Rendering Path

  1. 解析HTML并生成DOM树+下载解析CSS并生成CSSOM树
    • link标签当媒体查询不符合条件时会变成异步加载,不会阻塞渲染

  1. DOMCSSOM都解析完毕后,构建RenderObject渲染树
    (其中display:none的元素不会进入渲染树,而visibility:hidden会进入)
  2. Layout(布局)
    根据RenderObject渲染树和设备视口(viewport)大小计算出各DOM节点的位置、大小的像素值。
  3. Paint(绘制)
    在多个层上分别进行DOM 元素的绘制
  4. 渲染层合并 (Composite)
    之前步骤都在CPU中完成后,浏览器主进程将默认的图层和复合图层交给 GPU ,将各个图层合成(composite),最后显示出页面
  • GUI线程同时解析DOM和下载并解析CSS(两者独立并行、互不阻塞),当遇到JS时被阻塞,转入JS线程下载并执行脚本。脚本执行完毕后回到GUI线程。
  • DOMCSSOM都解析完毕后才会进行渲染,现代浏览器在GUI被JS阻塞时会将已有的GUI部分先显示,称为First Paint
  • 整个HTML文档(包括JS)解析完毕后触发DOMContentLoaded事件,然后等媒体资源(图片,音频,视频,iframe等)都加载完毕后再触发onload事件。
document.addEventListener('DOMContentLoaded', function () {});
window.onload = function(){}
2. JS引擎线程

一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
JS是单线程的,因为其设计用于进行用户操作和DOM交互,避免多线程在操作同一DOM时出现冲突,并需要引入锁等复杂概念
GUI 渲染线程与 JS 引擎线程是互斥的。执行JS线程时GUI线程会被暂时挂起,执行GUI线程时JS线程会被暂时挂起。因为如果 JS线程和 GUI线程同时运行,那么渲染线程前后获得的元素数据就可能不一致了

script标签属性作用

  • defer
    异步加载,并在所有元素解析完成之后,DOMContentLoaded事件触发前执行。
  • async
    加载和执行都变成异步。
    如果script标签是由JavaScript代码创建的,标签的async属性会默认为true
    蓝色:JS加载,红色:JS执行,绿色: HTML 解析
3. 事件触发线程

来自浏览器内核的事件及JS引擎中的异步任务会被添加进事件触发线程,当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎线程的处理

4. 定时触发器线程
5. 异步http请求线程

在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

6. Web Worker 线程 (IE11以上支持)

JS引擎线程向浏览器申请开一个子线程(仅能通过.postMessage()与JS线程交互,而且不能操作DOM)用于处理复杂JS,防止阻塞页面。
逻辑完成后应在主线程中调用.terminate()或Worker中调用close()方法关闭Worker以释放资源,否则会一直占用
postMessage传递对象时仅传值不传址(先将通信内容串行化,然后把串行化后的字符串发给 Worker,再还原。)

  • 主线程中
    • 引用的Web Worker的脚本文件必须和主线程同源
    • 通过.postMessage()发送消息,通过.onmessage()接收消息
    • 通过.onerror(function (event) {})或者.addEventListener('error', function (event) {})可监听Worker中的错误
var worker = new Worker('test.js');
worker.postMessage('Hello World');
worker.postMessage({ method: 'echo', args: ['Work'] });

worker.onmessage = function (event) {
    console.log('Received message ' + event.data);
    worker.terminate();
}
  • Worker线程中
    • Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用documentwindowparent等(没有alertconfirm方法),仅有navigator对象和location对象。
    • 全局对象可用selfthis表示,也可直接省略(同主线程中的window)
    • 通过addEventListener('message',function(e){})onmessage()监听主线程推送的消息,或通过postMessage()推送消息
    • 通过importScripts()加载其他js脚本
    • Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
// 写法零
self.addEventListener('message', function (e) {
  self.postMessage('You said: ' + e.data);
}, false);
// 写法一
this.addEventListener('message', function (e) {
  this.postMessage('You said: ' + e.data);
}, false);
// 写法二
addEventListener('message', function (e) {
  postMessage('You said: ' + e.data);
}, false);

Event Loop

同步任务都在JS线程上执行,形成一个执行栈。
JS线程之外,事件触发线程管理着一个任务队列,异步任务完成后会将其回调加入任务队列。
一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
定时器线程会在倒计时完成时将任务加入任务队列,但任务的执行依然得等到JS线程空闲,因此JS通过计时器执行任务是不准确的

macrotask与microtask
  • macrotask(又称之为宏任务或task)
    包括每次执行的 主代码块脚本执行渲染事件(如解析/绘制DOM)、setTimeoutsetIntervalpostMessagesetImmediate用户交互事件(如鼠标点击)、I/O相关(如XMLHttpRequest是网络I/O)等
    每一个task会从头到尾将这个任务执行完毕,不会被打断
    浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
    队列由事件触发线程维护(会进入任务队列)

  • microtask(又称为微任务或jobs)
    process.nextTick(高于其他微任务)Promise.then catch finally(注意不是 Promise主代码块)、MutationObserver、被 await 阻塞的语句等。
    在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕。在处理microtask期间,如果有新添加的microtasks,也会被添加到当前微任务队列的末尾
    队列由JS引擎线程维护(不进入任务队列,有自己专门的微任务队列)

    • Promise的polyfill,一般都是通过setTimeout模拟的,所以是macrotask形式
    setTimeout(function(){
        console.log(1)
    },0);
    new Promise(function(resolve){
        console.log(2)
        for( var i=100000 ; i>0 ; i-- ){
            i==1 && resolve()
        }
        console.log(3)
    }).then(function(){
        console.log(4)
    });
    console.log(5);

// 2 3 5 4 1
setTimeout和setInterval
  • setTimeout 过指定时间将回调函数加入队列
  • setInterval 每过指定时间将回调函数加入队列
    • 把浏览器最小化显示等操作时,setInterval的回调函数依然会进入队列,等浏览器窗口再次打开时,一瞬间全部执行
      部分浏览器会对setInterval进行优化,如果当前事件队列中有setInterval的回调,不会重复添加。
    • 一般认为的最佳方案是:用setTimeout模拟setInterval以保证两次回调之间的最小时间差,或者特殊场合直接用requestAnimationFrame
  • V8中用 32 个 bit 来存储延时值,其最大能存放的数字是 2147483647,因此如果设置的延迟值大于 2147483647 毫秒(大约 24.8 天)时就会溢出,定时器会被立即执行。
requestAnimationFrame和requestIdleCallback
  • requestAnimationFrame 在每次屏幕被刷新前加入调用队列
  • requestIdleCallback 在每次屏幕刷新并且空闲时加入调用队列(在requestAnimationFrame之后加入)
    requestIdleCallback(fn,{timeout:1000})即在下次刷新且空闲时加入调用队列,且最迟在1s后加入

注意requestAnimationFramerequestIdleCallback不是微任务,在某种程度上可以理解为宏任务

你可能感兴趣的:(浏览器的进程、线程、Web Worker及Event Loop)