什么是事件循环(event loop)
主线程不断的从消息队列中获取消息,执行消息,这个过程被称为事件循环,在javaScript中就是采用事件循环来解决单线程带来的问题
线程和进程
进程:计算机已经运行的程序,是操作系统管理程序的一种方式,我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程);
线程:操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中,每一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程;
目前多数浏览器其实都是多进程的,默认当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出
同步任务
在主线程上排队执行的任务,只有前一个任务执行完毕才能执行后一个任务
异步任务
不进入主线程而进入任务队列的任务,只有【任务队列】通知主线程,某个异步任务可以执行了,该任务才会进入主线程。
宏任务队列可以有多个,微任务队列只有一个
浏览器中的Event Loop
当 JS 引擎去执行 JS 代码的时候会从上至下按顺序执行,当遇到异步任务的,就会交由浏览器的其他线程去执行,处理完成就会通知事件触发线程将回调方法推送至事件任务队列的列尾,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行
宏任务队列(macro-task):setTimeOut、setInterval、<script></script> 整体代码、UI渲染、ajax
微任务队列(micro-task): Promise.then()或catch()、queueMicrotask()、MutationObserver
Node中的Event Loop
Node.js的运行机制如下:V8引擎解析JavaScript脚本。解析后的代码,调用Node API。libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎
宏任务(macro-task):setTimeOut、setInterval、IO事件、setImmediate、close事件
微任务(micro-task): Promise.then()或catch()、queueMicrotask()、process.nextTick
liubv引擎Event Loop的6个阶段介绍
timers 阶段
这个阶段执行timer(setTimeout、setInterval)的回调
timers是事件循环的第一个阶段,当我们使用setTimeOut或者是setInterval时,node会检查timers堆中有无过期的timer,如果有则依次执行
需要注意的是node不能保证到了过期时间就立即执行回调函数,因为他在执行回调前必须检查timer是否过期,检查的时候是需要消耗时间的,他可能不会立即看到过期的timer,从而略过,检查时间的长短取决于系统性能,性能越好执行的速度越快,另外一点是如果Event Loop中还有别的进程在执行,也会影响timer回调执行。
这与浏览器的Event Loop机制是类似的,浏览器环境中如果定时器在一个非常耗时的for循环之后运行,虽然时间已过期,仍要等到for循环计算完成才会执行定时器
在达到过期时间之间的时间称为有效期,定时器能够保证的就是在有效期内不会触发定时器
pending callbacks(pending callbacks) 阶段
这个阶段主要是执行某些系统层级操作的回调函数。比如说,TCP发生错误时候的错误回调。
idle 阶段
仅node内部使用。
prepare 阶段
只是表达空闲、预备状态(第二阶段结束,poll未触发之前)
poll 阶段:
检索新的I/O事件,执行I/O相关回调, 适当的条件下node将阻塞在这里
poll阶段主要有两个功能:
- 根据不同的操作系统的实际情况来计算轮询I/O的时间。
- 处理poll队列中的事件
如果poll queue不为空且没有到达限制,event loop将同步执行queue里的callback,直至queue为空,或执行的callback达到系统上限。
如果 poll queue为空,且代码未设定timer,将会发生下面情况:
- 如果setImmediate()设定了callback,event loop将结束poll阶段进入check阶段并执行回调
- 如果没有setImmediate回调,event loop将阻塞在该阶段等待回调加入到队列,然后立即执行
如果代码设定timer了,poll queue为空,event loop将检查times,如果有times时间已经到达,event loop将按循环进入times阶段执行timer queue
check 阶段
在轮询I/O之后执行一些事后工作,通常是执行 setImmediate() 的回调
这个阶段允许poll阶段结束后立即执行回调,如果poll阶段空闲并且有setImmediate回调,那么将结束poll阶段进入check阶段执行回调
setImmediate实际上是一个特殊的timer,跑在事件循环中的一个独立阶段,他使用libuv的API来设定poll阶段之后立即执行回调
close callbacks 阶段
执行一些关闭的回调函数,如执行 socket 的 close 事件回调
liubv引擎Event Loop的6个阶段执行
在node.js里,任何异步方法(除timer,close,setImmediate之外)完成时,都会将其callback加到poll queue里,并立即执行
event loop 的每个阶段都有一个该阶段对应的队列和一个microtask队列
一个阶段执行完毕进入下一个阶段之前,Event Loop会先清空microtask队列的任务(如果有nextTick队列,则先清空nextTick队列然后再清空microtask队列),等到microtask队列清空后再进入下一个阶段
当所有阶段被顺序执行一次后,称 event loop 完成了一个 tick
延伸问题
为什么js是单线程
这主要和js的用途有关,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom;这决定了它只能是单线程,否则会带来很复杂的同步问题。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。
浏览器多进程架构
下面主要介绍一下核心进程渲染进程
渲染进程(核心进程)
GUI线程
渲染布局
*主要负责页面的渲染,解析HTML、CSS,构建DOM树,布局和绘制等
*当页面需要重绘或者由于某种操作引发回流(重排)时,将执行该线程
*该线程与js引擎线程互斥,当执行js引擎线程时,GUI渲染会被挂起,当任务队列空闲时,主线程才会去执行GUI渲染
js引擎线程
解析、执行js,与GUI线程互斥
定时器触发线程
负责执行定时器的线程,如setTimeOut、setInterval
事件触发线程
主要负责将准备好的事件交给js引擎线程执行
如定时器结束,ajax等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将待发事件依次加入任务队列队尾,等待js引擎线程执行
异步HTTP请求线程
负责执行异步请求一类的函数线程,如Promise、axios。
参考文档
https://github.com/nodejs/help/issues/1118
https://cloud.tencent.com/developer/article/2137582
https://www.jianshu.com/p/71defc1226c8
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
https://blog.csdn.net/weixin_44685906/article/details/124636886