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

一些计算机概念

CPU

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


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

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


进程和线程

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

串行 并行 并发 协程

  • 串行 多个任务,执行时一个执行完再执行另一个。
  • 并行 多个任务同时执行。
  • 并发 一个CPU同时只能执行一个进程,其多个核心可以分别执行一个线程。系统不停切换线程,看起来像同时运行
  • 协程 进程和线程占用内存大,且需通过CPU调度切换,切换过程耗时长。而协程并不增加线程数,切换代价小。
  • 综上:
    单CPU中进程只能是并发,多CPU中进程可以并行;
    单CPU单核中线程只能并发,单CPU多核中线程可以并行。
    • 一个特例:Python GIL锁
      GIL实际上是一个互斥锁,在Python解释器层面上实现:同一时刻只有一个线程能够获得解释器的控制权,其他线程被阻塞。这意味着在多核CPU上,Python线程依然只能并发,不能并行

浏览器是多进程的

通过Chrome的更多工具 -> 任务管理器 可以查看进程信息

  • 每个网页(浏览器Tab)的渲染进程(Renderer)占用一个进程
  • 每种第三方插件占用一个进程
  • 所有网页公用一个Browser主进程(前进、后退、下载等)和一个GPU绘图进程
  • 每个Service Worker是一个单独的进程
Chrome=>更多工具=>任务管理器

网页的渲染进程(Render)

1. GUI渲染线程

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

  1. 解析HTML并生成DOM树+下载解析CSS并生成CSSOM树
    • link标签当媒体查询不符合条件时会变成异步加载,不会阻塞渲染
<link href="other.css" rel="stylesheet" media="(min-width: 40em)">
  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
    加载和执行都变成异步。
    由JavaScript代码创建的script标签,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不是微任务,在某种程度上可以理解为宏任务

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容