事件循环
之前了解的事件循环都只是了解到了同步,异步(宏任务(现在没有了),微任务)。但是今天发现不止有这两个队列,下面从0开始详细看一下真正的事件循环
浏览器的进程模型。
进程
什么是进程呢,进程就是程序所要运行需要开辟的空间,每个程序都有自己的进程空间,这层内存空间就可以理解为进程
每个程序都至少有一个进程,切进程之间都是相互独立(为的是一个进程出现问题不会影响到其它进程),即使需要进行通信的话,需要双发进行同意
线程
有进程以后就可以进行代码的运行了,但是需要有东西来进行运行,运行代码的被成为线程。一个进程的话最少是拥有一个线程的,所有在进程开启以后会自动创建一个线程来运行代码,改线程成为主线程。如果程序中需要执行多块代码,主线程就会启动更多线程来执行代码,所以,一个进程中可以包含多个线程,
浏览器有哪些进程和线程
浏览器不同于其它的,它是一个多进程,多线程的引用程序
浏览器内部工作是非常复杂的。
为了避免相互影响,减少连环崩溃的几率,当浏览器启动的时候会自动启动多个进程
可以在浏览器的任务管理中查看当前的所有进程。
其中,最主要的进程有:
1.浏览器进程
主要负责界面显示,用户交互,子线程管理等等。浏览器内部会启动多个线程来进行处理不同的任务。
2.网络进程
负责加载网络资源,网络进程内部也就有多个线程来进行处理不同的网络任务
3.渲染进程
渲染进程启动以后,会开启一个渲染主线程,主线程负责执行,HTML,css,js代码。默认情况下,浏览器会为每个页面都开启一个渲染进程(看下方图片),用来保证标签页之间不会相互影响。(每个页面一个进程,这就是谷歌吃性能的原因吧)。// 有可以能后面会改变,看的是后面是一个站点一个进程。这里就不详细讲解了。可以看一下 chrome官方说明文档
渲染主线程是如何工作的
渲染线程是浏览器线程中最繁忙的线程,需要它处理任务的任务包括,但不限于
- 解析html
- 解析css
- 计算样式
- 布局
- 处理图层
- 绘制页面
- 执行全局js代码
- 处理事件函数
- 处理回调函数
- 。。。。
为什么渲染进程不适用多个线程来处理这些事情?
渲染进程之所以不适用多个线程来处理这些任务,是因为这些任务之间存在着严格的依赖和顺序关系。
在渲染页面的过程中,需要依次进行 HTML 解析、CSS 解析、样式计算、布局和绘制等操作。这些任务之间存在着依赖关系,每个任务的执行结果会作为下一个任务的输入。如果使用多个线程同时执行这些任务,就会涉及到线程之间的同步和协调,增加了复杂性,并且由于任务之间的依赖关系,多线程并行执行也很难获得明显的性能提升。
此外,渲染进程还需要与浏览器的其他进程进行通信,例如网络进程、GPU 进程等。这些进程需要与渲染进程进行协作,以获取页面资源、处理图层、实现硬件加速等功能。如果渲染进程采用多个线程,就会增加进程间通信的复杂性和开销。
因此,为了确保渲染任务的正确有序执行,并简化进程间通信的复杂性,渲染进程通常会使用单个主线程来处理这些事情。当然,在一些较新的浏览器中,也引入了一些并行处理的机制,例如将某些计算任务或绘制任务交给其他线程处理,以提高性能和响应速度。但总体上,渲染进程的主线程仍然是最繁忙的线程,负责执行大部分的渲染任务。
要处理这么多任务,主线程要怎么调度任务
比如
-
我正在执行一个js函数,执行到一般的时候用户点击了按钮,要立即去执行点击事件的回调函数吗?
-
我正在执行一个js函数,执行到一半的时候某个计时器时间到了,要立即去执行它的回调函数吗?
-
浏览器进程通知我,"用户点击了按钮",此刻定时器也到达了事件,应该处理哪一个?
所以,渲染主线程就有了队列,来进行排队
1.最开始的时候,渲染进程会进入一个无限循环
2.每一次循环会检查消息队列中是否有任务存在。如果有,就取出来第一个任务执行,执行完后进如下一次循环,如果没有,就进入休眠状态
3.其它的所有线程(包括其它进程的线程)可以随时向消息队列添加任务。新任务会添加到消息队列的末尾。再添加新任务时,如果主线程时休眠状态,则会将其唤醒继续循环拿取任务
这样一来,就可以让每个任务有条不紊的、持续的进行下去了。
整个过程被成为事件循环(也叫做消息循环)
何为异步
代码在执行过程中会遇到一些无法立即处理的任务,比如
- 计时器完成后需要执行的任务
- 网络通信完成后需要执行的任务
- 用户操作以后需要执行的任务
如果让渲染主线程等待这些任务时机达到,就会导致长期处于阻塞
状态,从而导致浏览器卡死
渲染主线程承担着极其重要的工作,无论如何都不能堵塞
因此浏览器选择异步来解决这个问题
使用异步的方式,渲染主线程永不堵塞
如何理解js的异步呢
js是一门单线程的语言,这是因为它运行在浏览器的渲染主线程,而渲染主线程呢只有一个。
而渲染主线程呢承担着诸多工作,渲染页面,执行js都在其中运行。
如果采用同步的方式,极有可能造成页面的堵塞,从而导致任务队列的其它任务无法执行,这样一来一方面会导致繁忙的主线程白白耗费时间,另一方面导致页面无法及时更新,给用户造成卡死的现象。
所以浏览器采用异步的方法来避免。具体做法就是当某些任务发生时,比如,计时器,时间监听,网络。主线程就会将任务交给其它线程进行处理,自身立即结束任务的执行,转而执行后续代码。当其它线程完成时,将事先传递的回调函数包装成任务,加入消息队列的末尾排队,等待主线程的执行
这种异步情况下,浏览器永不堵塞,最大程度的保证了单线程的流畅运行
js堵塞渲染 看代码
javascript
<h1>123456</h1>
<button>change</button>
<script>
var h1 = document.querySelector('h1');
var btn = document.querySelector('button');
// 死循环指定的时间
function delay (duration) {
var start = Date.now();
while (Date.now() - start < duration) { }
}
btn.onclick = function () {
h1.textContent = '654321';
delay(3000);
};
</script>
点击button以后不会立即从123456变化为654321,因为它赋值以后事件已经结束,要是进行渲染的话还需要再次排队进行执行,而此刻还有一个循环的任务,所有需要三秒以后才会执行。
任务存不存在优先级呢
任务时不存在优先级的,在消息队列中,先进先出。但是消息队列存在
根据W3C最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。
- 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行
随着浏览器的复杂度急剧提示,W3C不再使用宏任务的说法
在目前chrome的实现中,至少包含了下面的队列
- 延时队列:用于存放计时器到达后的回调任务,优先级「中」
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
- 微队列:用户存放需要最快执行的任务,优先级「最高」//添加任务到微队列的主要方式主要是使用 Promise、MutationObserver
浏览器还有很多对象,但是和前端关系不大,感兴趣可以自行了解一下。
JS 的事件循环阐述概况
事件循环又叫做消息循环(chrome内部叫做 message loop),是浏览器渲染主线程的工作方式
在Chrome的源码中,他会开启一个不会结束的for循环,每次循环从消息队列中取出第一个任务执行,从而其它线程只需要在合适的时候将任务加入多队列末尾即可
过去吧消息队列简单分为了宏任务和微任务,这种说法现在已经无法满足复杂的浏览器环境,取而代之的是更灵活多变的处理方式
根据W3C官方的解释,每个任务又不同的类型,同一类型的任务必须在同一队列,不同的任务可以属于不同的队列,不同的任务又不同的优先级,再一次事件循环中,由浏览器自行决定取哪一个队列的任务,但浏览器必须又一个微队列,微队列的任务具有最高优先级,必须优先调度。
js计时器能做到精准计时吗
不能,因为
- 计算机硬件没有原子钟,无法做到精确计时
- 操作系统的计时函数本身就有少量偏差,由于js的计时器最终还是调用的操作系统的函数,也就携带了这些偏差
- 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层,则会带有 4 毫秒的最少时间,这样在计时时间少于 4 毫秒时又带来了偏差
- 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差