浏览器中的事件循环(Event Loop)机制
浏览器进程模型
谈到浏览器中的事件循环就不得不谈到浏览器的进程模型了,浏览器是一个多进程多线程的应用程序。其主要原因是因为:其内部工作极其复杂,为了不相互影响导致全部崩溃,因此当启动浏览器后他会启动多个进程进行相互协作,其中必不可少的为:
浏览器主进程
主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。
网络进程
负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。
GPU 进程
用于加速渲染进程中的图形和动画处理。它利用计算机的图形处理单元(GPU)来处理复杂的图形计算,如渲染3D效果、视频播放、图像处理等。GPU进程将渲染进程发送的图形命令转换为GPU可以理解的指令,并将渲染结果返回给渲染进程。
多个插件进程
主要是负责插件的运行,插件运行中可能会出现崩溃,为了防止由于插件崩溃的导致浏览器崩溃,因此使用插件线程来进行隔离,以确保插件崩溃时不会对页面和浏览器造成影响
多个渲染进程
渲染进程是负责处理网页内容渲染的进程。它负责解析HTML、CSS和JavaScript,将网页内容转换为可视化的图像,并将其显示在浏览器窗口中。渲染进程还处理用户交互、处理鼠标和键盘事件、执行JavaScript代码等。
默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响。未来可能调整为默认为每个域名为一个渲染进程。参考:chrome官方说明文档
渲染主进程如何工作
渲染主进程的工作过程又称为事件循环(消息循环),渲染主进程的工作包括但不限于如下:
- 解析 HTML
- 解析 CSS
- 计算样式
- 布局
- 处理图层
- 每秒把页面画 60 次
- 执行全局 JS 代码
- 执行事件处理函数
- 执行计时器的回调函数
由于该进程执行的任务非常之多,因此如果不进行任何处理而去顺序执行任务,则会造成浏览器的卡顿,例如:用户正在执行一个函数,执行过程中,另外一个计时器到达了执行时间,此时该如何执行。因此渲染进程采用了排队机制来解决这一系列的问题。其中排队的队列称为消息队列。因此浏览器只需要每次使用该队列的第一个任务(队列遵循先进先出的原则)即可(如果存在任务)。
- 在最开始的时候,渲染主线程会进入一个无限循环
- 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。
- 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务
异步任务
浏览器为了解决排队过程中造成的不必要等待,例如:无休止的等待定时器执行完毕、网络通信后的回调函数。从而有了异步任务。
由于渲染主线程只有一个,而 JS 工作在渲染主线程中,因此 JS 在此是一门单线程的语言。
渲染主线程承担着很多工作:渲染页面,执行 JS 等,如果使用同步的方式来执行任务,则极有可能造成渲染主线程阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。
因此浏览器采用异步的的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。
在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。
消息队列
任务没有优先级,但消息队列有优先级。
参考:event-loops
根据 W3C 的解释,消息队列(任务队列)指的的集合,而非队列。事件循环应该有一个或者多个任务队列,但微任务则不属于消息队列。
其实也就是说除了一个任务队和一个微任务队列
而消息队列根据任务类型可以划分多个队列,由浏览器根据自身实际情况自行判断优先级(先执行哪个队列的内容)。
在目前 chrome 的实现中,至少包含了下面的队列:
- 延时队列:用于存放计时器到达后的回调任务,优先级「中」
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
- 微队列:用户存放需要最快执行的任务,优先级「最高」
从代码中实践事件循环
1 | async function async1() { |
await
可以理解为Promise
的一个语法糖,例如如上await async2();
可以改写为:
1
2
3
4
5
6
7
8
9
10
11
12 new Promise( resolve =>{
async2()
resolve()
}).then( () => {
console.log("async1 end");
new Promise((resolve)=>{
console.log('promise4')
resolve()
}).then(()=>{
console.log('promise5')
})
})
因此对于上述代码的执行顺序如下:
首先加载所有代码,并依次执行所有同步代码,异步代码则丢入对应的队列由对应的线程进程处理。
主线程:
console.log("script start");
;将两个setTimeout
丢入到延迟队列;执行async1()
,进入该函数,执行同步代码console.log("async1 start");console.log("async2")
,遇到微任务(await后的即为微任务),将其丢入微任务队列,此时将暂时退出该函数,执行 35 行的 Promise 中的同步部分即console.log("promise1");resolve();
;最后执行console.log("script end")
,此时主线程的代码执行完毕,将在队列中进行取出任务并执行。微任务队列:
[async1函数产生的 Promise , 35行处产生的 Promise, ]
延迟队列:
[setTimeout1, setTimeout2]
按照队列优先级,从最高级优先的队列中执行第一个排队的任务:
async1函数产生的 Promise
,输出console.log("async1 end")
此时遇到 Promise 执行其同步部分console.log('promise4');resolve()
,并将异步内容再次推入到队列微任务队列:
[35行处产生的 Promise, async1中 Promise 产生的任务]
延迟队列:
[setTimeout1, setTimeout2]
此时渲染主线程再次从队列中读取任务,并执行
35行处产生的 Promise
中的console.log("promise2")
微任务队列:
[async1中 Promise 产生的任务]
延迟队列:
[setTimeout1, setTimeout2]
继续执行微任务队列中的任务:
console.log('promise5')
此时微任务队列为空,则执行下一个队列中的任务
console.log("setTimeout1");console.log('promise6')
并产生微任务,将其推入到微任务队列微任务队列:
[setTimeout1产生的微任务]
延迟队列:
[setTimeout2]
此时微任务队列又有了任务,因此将再次执行微任务队列中的任务
console.log('promise3')
此时只剩下延迟队列中的
console.log("setTimeout2")
函数执行
1 | script start |
参考
此文虽然有参考,但还是有些内容无法从参考文中得到明确的答案,因此部分结论为我个人根据资料推测得出。
如有错误,还望大佬指正。
《JavaScript忍者秘籍 第二版》深入事件循环部分