从浏览器多进程到JS单线程,全面梳理了解JS运行机制

一、区分线程和进程

如何区分线程和进程?先看看下面这个形象的比喻:

进程是一个工厂,线程是工厂中的工人。
工厂有它的独立资源 - 工厂之间相互独立 - 工厂内有一个或多个工人 - 工人之间共享空间 - 多个工人协作完成任务。

如果是 windows 电脑中,可以打开任务管理器,可以看到有一个后台进程列表。对,那里就是查看进程的地方,而且可以看到每个进程的内存资源信息以及 cpu 占有率。
在这里插入图片描述
所以,应该更容易理解了:进程是cpu资源分配的最小单位(系统会给它分配内存)。

进程是 cpu 资源分配的最小单位(是能拥有资源和独立运行的最小单位)。
线程是 cpu 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)。

不同进程之间也可以通信,不过代价较大。

现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多(所以核心还是得属于一个进程才行)。

二、浏览器是多进程的

理解了进程与线程了区别后,接下来对浏览器进行一定程度上的认识:(先看下简化理解)

浏览器是多进程的

浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)

简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。

关于以上几点的验证,请再第一张图:
在这里插入图片描述
图中打开了Chrome浏览器的多个标签页,然后可以在Chrome的任务管理器中看到有多个进程(一个主进程,以及每一个Tab页面分别有一个独立的进程)。

注意:在这里浏览器应该也有自己的优化机制,有时候打开多个tab页后,可以在Chrome任务管理器中看到,有些进程被合并了(所以每一个Tab标签对应一个进程并不一定是绝对的)。譬如打开多个空白标签页后,会发现多个空白标签页被合并成了一个进程,如图:
在这里插入图片描述

三、浏览器都包含哪些进程?

1.Browser进程(也叫控制进程)

浏览器的主进程(负责协调、主控),只有一个。作用如下:

  • 负责浏览器界面显示,与用户交互。如前进,后退等;
  • 负责各个页面的管理,创建和销毁其他进程;
  • 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上;
  • 网络资源的管理,下载等;

2.第三方插件进程

每种类型的插件对应一个进程,仅当使用该插件时才创建。

3.GPU进程

最多一个,用于3D绘制等。

4.浏览器渲染进程(也叫浏览器内核 / Renderer进程)

其内部是多线程的。默认每个Tab页面一个进程,互不影响。作用如下:

页面渲染,脚本执行,事件处理等(这些后面重点讲)。

四、浏览器多进程的优势

相比于单进程浏览器,多进程有如下优点:

  • 避免单个page crash影响整个浏览器;
  • 避免第三方插件crash影响整个浏览器;
  • 多进程充分利用多核优势;
  • 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性;

简单点理解:如果浏览器是单进程,那么某个Tab页崩溃了或者插件崩溃了,就影响了整个浏览器。

当然,内存等资源消耗也会更大,有点空间换时间的意思。

五、重点是浏览器内核(渲染进程)

重点来了,我们可以看到,上面提到了这么多的进程,那么,对于普通的前端操作来说,最终要的是什么呢?答案是渲染进程。

浏览器的渲染进程是多线程的。

渲染进程的主要常驻线程:

1.GUI渲染线程

  • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等;
  • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行;

2.JS引擎线程(JS内核)

  • 负责处理Javascript脚本程序(例如V8引擎);
  • 负责解析Javascript脚本,运行代码;
  • JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序;

注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

3.事件触发线程

  • 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助);
  • 当JS引擎执行代码块如setTimeOut时(或是来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中;
  • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理;
  • 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行);

4.定时触发器线程

  • 传说中的setInterval与setTimeout所在线程;
  • 浏览器定时计数器并不是由JavaScript引擎计数的(因为JavaScript引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确);
  • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行);
  • 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms;

5.异步http请求线程

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

六、为什么JS引擎是单线程的?

JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript 不能有多个线程呢 ?这样能提高效率啊。

JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。

七、Browser进程和Renderer进程的通信过程

看到这里,应该对浏览器内的进程和线程都有一定理解了,那么接下来,再谈谈浏览器的Browser进程(浏览器主控进程)是如何和Renderer进程(浏览器内核)通信的。这点也理解后,就可以将这部分的知识串联起来,从头到尾有一个完整的概念。

如果自己打开任务管理器,然后打开一个浏览器,就可以看到:任务管理器中出现了两个进程(一个是主控进程,一个则是打开Tab页的渲染进程),然后在这前提下,简单看下整个通信过程:

  • Browser进程收到用户请求,首先需要获取页面内容(譬如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render进程;
  • Renderer进程的Renderer接口收到消息,简单解释后,交给渲染线程,然后开始渲染;
  • 渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染;
  • 当然可能会有JS线程操作DOM(这样可能会造成回流并重绘);
  • 最后Render进程将结果传递给Browser进程;
  • Browser进程接收到结果并将结果绘制出来;
    在这里插入图片描述

看完这一整套流程,应该对浏览器的运作有了一定理解了,这样有了知识架构的基础后,后续就方便往上填充内容。

八、浏览器渲染流程

为了简化理解,前期工作直接省略成:(要展开的或完全可以写另一篇超长文)

浏览器输入url,浏览器主进程接管,开一个下载线程,然后进行http请求(略去DNS查询,IP寻址等等操作),然后等待响应,获取内容,随后将内容通过RendererHost接口转交给Renderer进程,浏览器渲染流程开始。

浏览器器内核拿到内容后,渲染大概可以划分成以下几个步骤:

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

渲染完毕后就是load事件了,之后就是自己的JS逻辑处理了。

下面提一些可能需要注意的细节:

1.load事件与DOMContentLoaded事件的先后

上面提到,渲染完毕后会触发load事件,那么你能分清楚load事件与DOMContentLoaded事件的先后么?

当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片。(譬如如果有async加载的脚本就不一定完成)

当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了。(渲染完毕了)

所以,顺序是:DOMContentLoaded => load

2.css加载是否会阻塞dom树渲染?

这里说的是头部引入css的情况。

首先,我们都知道:css是由单独的下载线程异步下载的

然后再说下几个现象:

css加载不会阻塞DOM树解析(异步加载时DOM照常构建),但会阻塞render树渲染(渲染时需等css加载完毕,因为render树需要css信息)

这可能也是浏览器的一种优化机制。

因为你加载css的时候,可能会修改下面DOM节点的样式,如果css加载不阻塞render树渲染的话,那么当css加载完之后,render树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。

所以干脆就先把DOM树的结构先解析完,把可以做的工作做完,然后等你css加载完之后,在根据最终的样式来渲染render树,这种做法性能方面确实会比较好一点。

3.普通图层和复合图层

渲染步骤中就提到了composite概念。可以简单的这样理解,浏览器渲染的图层一般包含两大类:普通图层以及复合图层。

  • 首先,普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中);
  • 其次,absolute布局(fixed也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层;
  • 然后,可以通过硬件加速的方式,声明一个新的复合图层,它会单独分配资源(当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘);

可以简单理解下:GPU中,各个复合图层是单独绘制的,所以互不影响,这也是为什么某些场景硬件加速效果一级棒。

注意:

一般一个元素开启硬件加速后会变成复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能;
但是,尽量不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡;

九、从Event Loop谈JS的运行机制

浏览器页面初次渲染完毕后,就到了JS引擎的一些运行机制分析。

在了解JS运行机制之前,我们先看一个问题:

JavaScript是单线程,怎样执行异步的代码 ?

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

js 引擎执行异步代码之所以不用等待,是因有为有消息队列和事件循环。

消息队列:消息队列(也叫任务队列)是一个先进先出的队列,它里面存放着各种消息(任务)。
事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。

实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。

那么,消息队列中放的消息具体是什么东西?我们可以认为:

消息就是注册异步任务时添加的回调函数。

总结:

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

看到这里,应该就可以理解:为什么有时候setTimeout推入的事件不能准时执行?因为可能在它推入到事件列表时,主线程还不空闲,正在执行其它代码,所以自然有误差。

十、单独说说定时器

在调用setTimeout后,是如何等待特定时间后才添加到事件队列中的?

是JS引擎检测的么?当然不是了。它是由定时器线程控制(因为JS引擎自己都忙不过来,根本无暇分身)。

为什么要有单独的定时器线程?因为JavaScript引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确,因此很有必要单独开一个线程用来计时。

什么时候会用到定时器线程?当使用setTimeout或setInterval时,它需要定时器线程计时,计时完成后就会将特定的事件推入事件队列中。

例如:

setTimeout(function(){console.log('hello!');}, 1000);

这段代码的作用是当1000毫秒计时完毕后(由定时器线程计时),将回调函数推入事件队列中,等待主线程执行。

setTimeout(function(){console.log('hello!');}, 0);
console.log('begin');

//begin 
//hello

这段代码的效果是最快的时间内将回调函数推入事件队列中,等待主线程执行。

注意:

只有可执行栈内空了后才会主动读取事件队列。具体的setTimeout运用场景点击这里查看。

十一、事件循环进阶:macrotask与microtask

上文中将JS事件循环机制梳理了一遍,在ES5的情况是够用了,但是在ES6盛行的现在,仍然会遇到一些问题,例如下面这题:

console.log('start');
setTimeout(function(){
	console.log('setTimeout');
}, 0);
Promise.resolve()
.then(function(){
	console.log('promise1');
})
.then(function(){
	console.log('promise2');}
);
console.log('end');

执行顺序如下:

start
end 
promise1 
promise2 
setTimeout

为什么呢?在JS中分两种任务类型:macrotask和microtask,在ECMAScript中,macrotask称为task,microtask称为jobs。

它们的定义和区别如下:

  • macrotask(宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行);
  • 每一个task会从头到尾将这个任务执行完毕,不会执行其它;
  • 浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染(task->渲染->task->…);
  • microtask(微任务),可以理解是在当前 task 执行结束后立即执行的任务;
  • 也就是说,在当前task任务后,下一个task之前,在渲染之前;
  • 所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染;
  • 也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前);

在什么样的场景会形成macrotask和microtask呢?

  • macrotask(宏任务):主代码块,setTimeout,setInterval等(可以看到,事件队列中的每一个事件都是一个macrotask);
  • microtask(微任务):Promise,process.nextTick等,并且在node环境下,process.nextTick的优先级高于Promise;

我们以 setTimeout、process.nextTick、promise 为例再次直观感受下两种任务队列的运行方式:

console.log('main1');
process.nextTick(function() {
    console.log('process.nextTick1');
});
setTimeout(function() {
    console.log('setTimeout');
    process.nextTick(function() {
        console.log('process.nextTick2');
    });
}, 0);
new Promise(function(resolve, reject) {
    console.log('promise');
    resolve();
}).then(function() {
    console.log('promise then');
});
console.log('main2');

别着急看答案,先以上面的理论自己想想,运行结果会是啥?

最终结果是这样的:

main1
promise
main2
process.nextTick1
promise then
setTimeout
process.nextTick2

参考文章

https://mp.weixin.qq.com/s/vIKDUrbuxVNQMi_g_fiwUA
https://www.jianshu.com/p/f478f15c1671

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值