这一章由标梵互动带领我们了解js的工作原理,只有了解了js的工作原理,才能写出更漂亮的代码,提高运行效率,解决开发中遇到的无法理解的问题。
![d6201d4690ddc2f108e53282fa4458dc.png](https://i-blog.csdnimg.cn/blog_migrate/abe730d6e56bc4b30287bd667f1a2521.jpeg)
进程和线程。
进程是cpu资源分配的最小单位,它可以包含多个线程。浏览器是多进程的,打开的每个浏览器窗口都是一个进程。
线程是cpu调度的最小单位,同一进程中的所有线程共享程序的内存空间。
流程可以看成一个仓库,螺纹是可以运输的卡车。每个仓库都有自己的卡车为仓库服务(送货)。每个仓库可以同时被多辆卡车拉,但是每辆卡车只能同时做一件事,就是这次运输货物。所以很好理解。
渲染过程。
浏览器由四个进程组成:
主进程(Browserprocess),浏览器只有一个主进程,负责资源下载、界面显示等主要基本功能。
GPU进程,负责三维图形绘制。
第三方插件流程,负责第三方插件处理。
渲染过程(Rendererprocess),负责js执行、页面渲染等功能,也是本章的重点内容。
渲染过程主要包括GUI渲染线程、Js引擎线程、事件循环线程、定时器线程和http异步线程。
GUI渲染线程。
首先看看浏览器在获得网站资源后做了什么:
浏览器首先会对html代码进行解析(其实html代码的本质就是一个字符串),并将其转换成浏览器能识别的一个节点,生成一个DOMTree,也就是一个DOM树。
然后解析css生成CSSOM(CSS规则树)
结合DOMTree和CSSOM生成渲染树
GUI就是来做这个的。如果某些元素的颜色或背景色被修改,页面将重新绘制。如果修改了元素的大小,页面将会重排。当页面需要重排版和重排版时,图形用户界面经常会执行和绘制页面。
这里有个提示:Reflow比Repaint贵,那么js性能优化中如何避免Reflow和Repaint呢?
JS引擎线程。
js引擎线程是js内核,负责解析和执行js代码,也称为主线程。同时浏览器只能有一个js引擎线程运行JS程序,所以JS是单线程运行的。
需要注意的是,js引擎线程和GUI渲染线程同时只能有一个作业,js引擎线程会阻塞GUI渲染线程。
<html>
<body>
<div id="div1"> a </div>
<script>
document.getElementById('div1').innerHTML = 'b'
</script>
<div id='div2'> div2 </div>
</body>
</html>
在浏览器渲染的时候遇到<script>标签,就会停止GUI的渲染,然后js引擎线程开始工作,执行里面的js代码,等js执行完毕,js引擎线程停止工作,GUI继续渲染下面的内容。所以如果js执行时间太长就会造成页面卡顿的情况,这也是后面性能优化的点。
事件循环线程
事件循环线程用来管理控制事件循环,并且管理着一个事件队列(task queue),当js执行碰到事件绑定和一些异步操作时,会把对应的事件添加到对应的线程中(比如定时器操作,便把定时器事件添加到定时器线程),等异步事件有了结果,便把他们的回调操作添加到事件队列,等待js引擎线程空闲时来处理。
定时器线程
由于js是单线程运行,所以不能抽出时间来计时,只能另开辟一个线程来处理定时器任务,等计时完成,把定时器要执行的操作添加到事件任务队列尾,等待js引擎线程来处理。这个线程就是定时器线程。
异步请求线程
当执行到一个http异步请求时,便把异步请求事件添加到异步请求线程,等收到响应(准确来说应该是http状态变化),把回调函数添加到事件队列,等待js引擎线程来执行。
Event Loop
上面介绍了渲染进程中的5个主要的线程,可能看完上面对各个线程简单的介绍,还有点不明白他们之间到底怎么协作工作的,下面山东标梵互动就从Event Loop的角度来聊一聊他们之间是怎样那么愉快合作的。
已经知道了js是单线程运行的,也知道js中有同步操作和异步操作。同步和异步大家应该很熟了,不多介绍。
同步操作运行在js引擎线程(主线程)上,会形成一个执行栈,而异步操作则在他们对应的异步线程上处理(比如:定时操作在定时器线程上;http请求则在异步请求线程上处理)。
而事件循环线程则监视着这些异步线程们,等异步线程们里面的操作有了结果(比如:定时器计时完成,或者http请求获取到响应),便把他们的毁掉函数添加到事件队列尾部,整个过程中执行栈、事件队列就构成Event Loop。
有关定时器(setTimeout、setInterval)的更多趣事
定时器会按照规定时间执行吗?
定时器是规定在一段时间之后执行一段代码,但是在js执行中不会准确无误的按照预期的时间去执行定时器里面的代码。
一个原因是W3C标准规定setTimeout中最小的时间周期是4毫秒,凡是低于4ms的时间间隔都按照4ms来处理。
其实还有一个重要的原因,如果仔细看上面的文章,大家应该会想到在js执行的时候,主线程碰到定时器的时候,是不会直接处理的,应该是先把定时器事件交给定时器线程去处理,这时主线程继续执行下面的代码,同时定时器线程开始计时处理,等到计时完毕,事件循环线程会把定时器要执行的操作放在事件队列末尾,等主线程空闲的时候再来执行事件队列里面的操作。
应该使用setTimeout还是setInterval
使用setTimeout模拟setInterval代码类似以下代码:
var say = function() {
setTimeout(say, 1000)
console.log('hello world')
}
setTimeout(say, 1000)
这样,当js遇到定时器时,就会交给定时器线程处理。计时完成后,计时器中的操作将被添加到事件队列中。当主线程空闲执行时,会遇到主线程执行时的定时器。这是上面的一系列操作。
你会发现这样做会在每次定时器执行的时候启动下一个定时器,错误只是等待主线程空闲的时间。
SetInterval就是定期把一个事件推入定时器线程,这就有一个问题,就是累积效应。
累积效应:
也就是说,如果在定时器中执行代码所需的时间比定时器的执行周期长,就会出现累积效应。简单来说就是上一次定时器中的操作没有完成,下一次定时器事件又来了。
累积效应会导致一些事件的丢失,以及丢失的原因。有兴趣的可以看看这篇文章,为了安全起见,尽量用setTimeout而不是setInterval。
如果有对setTimeout非常感兴趣的同学,我强烈建议你看一下80%考生不及格的JS面试题。
宏任务和微任务。
Microtask是Promise中的一个新概念。
宏任务.
宏任务中的事件都放在事件队列中,由事件触发线程维护。
宏任务(Macrotask),可以理解为执行栈每次执行的代码都是宏任务(包括每次从事件队列中获取一个事件回调,放入执行栈执行)
每个任务都会从头到尾的完成这个任务,不会执行其他任何事情。
为了让JS内部任务和DOM任务有序执行,浏览器会在一个任务执行完之后,下一个任务执行之前,重新渲染页面。
microtask.
microtask(也称为microtask)可以理解为在当前任务执行后立即执行的任务。
微任务中的所有微任务都被添加到由JS引擎线程维护的微任务作业队列中。
在当前任务之后、下一个任务之前和渲染之前执行。
所以它的响应速度比setTimeout(setTimeout是任务)快,因为不需要等待渲染。
也就是说,在宏任务执行之后,在其执行期间生成的所有微任务都将被执行(在渲染之前)
因此,js运行过程:
执行宏任务(如果它不在堆栈中,则从事件队列中获取它)
如果在执行过程中遇到微任务,它将被添加到微任务的任务队列中。
宏任务执行后,立即执行当前微任务队列中的所有微任务(按顺序执行)
当前宏任务完成后,检查渲染,然后GUI线程接管渲染。
渲染后,JS线程继续接管并启动下一个宏任务(从事件队列中获取)
宏任务和微任务的分析借鉴了浏览器多进程到JS单线程的JS运行机制最全面的梳理。