关于JS的运行机制

一、进程与线程

1、进程

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

2、线程

线程是CPU调度的最小单位(线程是建立在进程基础上的一次程序运行单位,一个进程中可以有多个线程)

3、区分进程与线程,且对它们的理解

1、进程是一个工厂,工厂有它的独立资源         ——》  工厂的资源指的是系统分配的内存(独立的内存)
2、工厂之间相互独立                        ——》   进程之间相互独立
3、线程是工厂中的工人,多个工人协作完成任务    ——》    多个线程在进程中协作完成任务
4、工厂内有一个或多个工人                   ——》      一个进程由一个或多个线程组成
5、工人之间共享空间                        ——》    同一个进程下的各个进程之间共享程序的内存空间(包括代码段、数据集、堆)

4、补充

(1)不同进程间可以进行通信,不过代价比较大
(2)单线程与多线程都是指在一个进程内的单和多

二、关于浏览器的进程和线程

1、简单理解浏览器

(1)浏览器是多线程的
(2)浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
(3)简单理解,每打开一个Tab标签页,就相当于创建了一个浏览器进程(但是这不是绝对的,浏览器有自己的优化机制,对于某些进程会进行合并,比如两个空白的Tab标签页)

2、五大浏览器与四大内核

谷歌(chrome):统称为Chrome内核,以前是Webkit内核,后改为Blink内核
IE:Trident内核,也就是俗称的IE内核
火狐(Firefox):Gecko内核,俗称Firefox内核
Safari(苹果):Webkit内核
Opera:最初是自己的Presto内核,后来是Webkit,现在是Blink内核

3、浏览器包含哪些进程

(1)Browser进程:浏览器的主要进程(负责协调、主控),只有一个,作用:
a、负责浏览器的显示,与用户交互,如前进、后退
b、负责各个页面的管理,创建和销毁其他进程
c、将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
d、网络资源的管理、下载等
(2)第三方插件进程:每种类型的插件对应一个进程,仅当使用插件时才创建
(3)GPU进程:最多一个,用于3D绘制
(4)浏览器渲染进程(浏览器内核,Render进程,内部是多线程的),默认每个Tab页面一个进程,互不影响,主要作用是:页面渲染、脚本执行、事件处理等

4、浏览器多进程的优势

(1)避免单个page crash影响整个浏览器
(2)避免第三方插件crash影响整个浏览器
(3)多进程充分利用多核优势
(4)方便使用沙盒模型隔离插件等进程,提高浏览器稳定性
如果浏览器是单进程的,那么一个Tab标签页崩溃了,就影响了整个浏览器,体验有多差;同理,如果是单进程,插件崩溃也会影响整个浏览器

5、浏览器的渲染进程

浏览器的渲染进程是多线程的,页面的渲染、JS的执行,事件的循环,都在这个进程内进行

渲染进程中包含的线程:
(1)GUI渲染线程:
a、负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等
b、当界面需要重绘或由于某种操作引发回流时,该线程就会执行
c、GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时,GUI进程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
(2)JS引擎线程:
a、也称为JS内核,负责处理JavaScript脚本程序(如V8引擎)
b、JS引擎线程负责解析javaScript脚本,运行代码
c、JS引擎一直等着任务队列中的任务到来,然后加以处理,一个Tab(renderer进程)中无论什么时候只有一个JS线程在运行JS程序
d、GUI渲染进程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞
(3)事件触发线程:
a、归属于浏览器而不是JS引擎,用来控制事件循环(可以理解为JS引擎自己都忙不过来,需要浏览器另开线程协助)
b、当JS引擎执行代码块如setTimeOut时(也可以来自浏览器内核的其他线程,如鼠标点击,AJAX异步请求等),会将对应的任务添加到事件线程中
c、当对应的事件符合条件被触发时,该线程会将事件添加至处理队列的队尾,等待JS引擎的处理
d、由于JS是单线程的,所以这些待处理队列中的事件都得排队等待JS引擎的处理
(4)定时触发器线程
a、传说中的setInterval与setTimeout所在线程
b、浏览器定时计数器并不是由javaScript引擎计数的(因为javaScript引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确)
c、因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
d、W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔为4ms
(5)异步http请求线程
a、在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
b、在检测到状态变更时,如果设有回调函数,异步线程就会产生状态变更事件,将这个回调再放入事件队列中,再由javaScript引擎执行

6、Browser进程和浏览器内核(Renderer进程)的通信过程

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

7、浏览器内核中线程之间的关系

(1) GUI渲染线程与JS引擎线程互斥

由于javaScript是可操纵DOM的,如果在修改这些元素属性的同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得元素数据就可能不一致了。

因此为了防止渲染出现不可 预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时,GUI引擎会被挂起,

GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行

(2) JS阻塞页面加载

从上述的互斥关系,我们知道,JS如果执行时间过长就会阻塞页面

譬如,假设JS引擎正在进行巨量的计算,此时就算GUI有更新,也会被保存在队列中,等JS引擎空闲后执行

然后由于巨量的计算,所以JS引擎可能很久很久后才能空闲

所要避免JS执行时间过长,这样就会造成页面渲染的不流畅,导致页面渲染加载阻塞的感觉
(3) 关于WebWorker,JS的多线程
JS是单线程的,而且JS执行时间过长会阻塞页面,那么JS就真的对cpu密集型计算无能为力?

H5中支持了Web Worker

官方解释为:

Web Worker为web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面
一个worker是使用一个构造函数创建的一个对象(e.g.worker())运行一个命名的javaScript文件
这个文件包含将在工作线程中运行的代码,workers运行在另一个全局上下文中,不同于当前的window
因此使用window快捷方式获取当点全局的范围(而不是self)一个worker内将返回错误

创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)

所以如果有非常耗时的工作,请单独开一个Worker线程,这样里面不管如何翻天复地都不会影响JS引擎主线程,只要等待计算出结果后,将结果通信传给主线程即可

而且,值得一提的是,Js引擎是单线程的,这一点的本质仍未改变,Worker可以理解为浏览器给JS引擎开的外挂,专门用来解决那些大量的计算问题

(4) WebWorker与ShareWorker
(a)WebWorker只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享, 所以Chrome在Render进程中,
(每一个Tab页就是一个render进程)创建一个新的线程来运行Worker中的javaScript程序

(b)ShareWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程所共享使用
所以Chrome浏览器为SharedWorker单独创建一个进程来运行javaScript程序,在浏览器中每个相同的javaScript只存在于一个SharedWorker进程,不管它被创建多少次

所以它俩本质上就是进程与线程的区别,SharedWorker由独立的进程管理,WebWorker只属于Render进程下的一个线程

三、简单梳理下浏览器的渲染流程

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

2、浏览器渲染流程开始

a、解析HTML拿到DOM树
b、解析CSS构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
c、布局render树,负责各元素尺寸、位置的计算
c、绘制render树,绘制页面像素信息
5、浏览器会将各层的信息发送给GPU,GPU将会将各层合成,显示到屏幕上

渲染完毕后,就是load事件

3、load事件与DOMContentLoaded事件的先后

顺序是,DOMContentLoaded ——》load

	当DOMContentLoaded事件触发时,仅当DOM树加载完毕,不包括样式表,图片(譬如如果有async加载的脚本就不一定完成)
	当onload事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完毕

4、CSS加载是否会阻塞dom树

这里说的是头部引入CSS的情况

CSS是由单独的下载线程异步下载的

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

因为加载CSS的时候,可能会修改下面DOM节点的样式
如果CSS加载不阻塞render渲染的话,那么当CSS加载完毕之后
render树可能又得重绘或者回流了,这就造成了一些没有必要的损耗
所以干脆就先把DOM树的结构西安解析完,把可以做的工作做完,然后等你CSS加载完之后,根据最终的样式来渲染render树

5、普通图层和复合图层

(1)浏览器渲染的图层一般包括两大类:普通图层(渲染图层)和复合图层
a、普通图层:是页面普通的文档流,我们虽然可以通过绝对定位、相对定位、浮动定位脱离文档流,但是它仍然属于默认复合层,共用一个绘图上下文对象
b、复合图层:又称图形层。它会单独分配资源,每个复合图层都有一个独立的图形上下文(当然也会脱离普通文档流,这样一来,不管这个复合图层中国怎么变化,也不会影响复合层里的回流重绘)

GPU中各个复合图层都是单独绘制的,所以互不影响

(2)如何变成复合图层(硬件加速)

a、最常用的方式:translate3d、translateZ
b、opacity属性/过渡动画(需要在动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之间的状态)
c、will-chang属性(这个比较偏僻),一般配合opacity与translate使用(而且经测试,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层)
d、<video><iframe><canvas><webgl>等元素
e、其它,譬如以前的flash插件

(3)绝对定位与硬件加速的区别

绝对定位虽然可以脱离普通的文档流,但是无法脱离默认的复合层。所以就算绝对定位中的信息改变时不会改变普通文档流中render树,但是浏览器最终绘制的时候,是整个复合层绘制的,所以绝对层中的信息的改变,仍然会影响整个复合层的绘制
而硬件加速直接就是在另一个复合层了,所以它的信息不会影响默认复合层

(4)复合图层的作用

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

四、从事件循环来谈JS的运行机制

1、首先我们简单理解几个概念

(1)JS分为同步任务和异步任务
(2)同步任务都在主线程上执行,形成一个执行栈
(3)主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列中放置一个事件
(4)一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行

在这里插入图片描述

下图中描述的是:
1、主线程运行时会产生执行栈(栈中的代码在调用某些API的时候,会在事件队列中添加各种事件)
2、栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调,如此循环
3、注意,总是要等栈中代码执行完毕之后才会去读取事件队列中事件

吃的是草

五、定时器

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

并不是由JS引擎检测的,是由定时器线程控制的(因为JS引擎自己都忙不过来)

2、为啥要单独的定时器线程

因为JS引擎是单线程的,如果处于阻塞线程的状态就会影响计时的准确,因此很有必要单独开一个线程来计时

3、何时会用到定时器线程

当使用setTimeout或setInterval时,他需要定时器线程,计时完成后就会将特定的事件推入事件队列中

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

在1000毫秒之后,会将回调函数推入事件队列中去,等待主线程的执行

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

console.log('begin');

这段代码的效果是最快的时间,但是执行结果依然是先 begin 再然后是 hello
虽然代码的本意是0毫秒之后就推入事件队列,但是W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔为4ms
就算不等待4ms,就算假设0毫秒就推入事件队列,也会先执行begin(因为只有可执行栈空了后才会主动读取事件队列)

4、用setTimeout而不是setInterval

用setTimeout模拟定期计时和直接用setInterval是有区别的。

因为每次setTimeout计时到后就会去执行,然后执行一段时间后才会继续setTimeout,中间就多了误差
(误差多少与代码执行时间有关)

而setInterval则是每次都精确的隔一段时间推入一个事件
(但是,事件的实际执行时间不一定就准确,还有可能是这个事件还没执行完毕,下一个事件就来了)

而且setInterval有一些比较致命的问题就是:

累计效应(上面提到的),如果setInterval代码在(setInterval)再次添加到队列之前还没有完成执行,
就会导致定时器代码连续运行好几次,而之间没有间隔。
就算正常间隔执行,多个setInterval的代码执行时间可能会比预期小(因为代码执行需要一定时间)

六、事件循环进阶:macrotask(宏任务)与microtask(微任务)

console.log('script start');

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

Promise.resolve().then(function() {
    console.log('promise1');
}).then(function() {
    console.log('promise2');
});

console.log('script end');

//执行结果
//script start
//script end
//promise1
//promise2
//setTimeout

为何执行结果如此呢?

1、JS分为两种任务类型:macrotaskmicrotask

(1)macrotask:
   宏任务,可以理解为每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件并回调放到执行栈中去执行)
  每一个task从头到尾将这个任务执行完毕,不会执行其它
  浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个task执行前渲染页面

(2)microtask:
(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务
也就是说,在当前task任务后,下一个task之前,在渲染之前
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染
也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)

2、分别很么样的场景会形成macrotask和microtask呢?

(1)macrotask:主代码块,setTimeout,setInterval等(可以看到,事件队列中的每一个事件都是一个macrotask)
(2)microtask:Promise,process.nextTick等
(3)补充:在node环境下,process.nextTick的优先级高于Promise__,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分。

3、根据线程理解

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

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

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值