什么是事件循环?
简单来讲事件循环机制是用于管理异步API
的回调函数什么时候能回到主线程中执行。
JS一般情况下在浏览器和Node.js
两种环境下运行为主,那么接下来分别介绍JS在这两种环境下的事件循环。
浏览器
这边先介绍一下,浏览器的渲染进程
渲染进程
主要负责界面显示、用户交互、子进程管理等。浏览器内部会启动多个线程处理不同的任务。
当中会创建唯一个渲染主线程,渲染主线程的工作非常之多:
- 解析 HTML
- 解析 CSS
- 计算样式
- 布局
- 处理图层
- 每秒把页面画 60 次
- 执行全局 JS 代码
- 执行事件处理函数
- 执行计时器的回调函数
- ......
也就是说,HTML、CSS、包括绘制和JS代码
都是在这里执行的。
接下来介绍一下何为异步?
异步
JS是一门单线程
的语言,这是因为它运行在浏览器的渲染主线程
中,而渲染主线程只有一个。而渲染主线程承担着诸多的工作,渲染页面,执行js都在其中运行。如果使用同步的方式,就极有可能导致主线程产生阻塞,白白消耗浏览器性能的同时还会出现页面卡屏现象。
所以浏览器采用异步
的方式来避免。具体做法是当某些任务发生时,比如计时器,网络,事件监听,主线程将任务交给其他线程去处理,跳过这些异步代码往后执行。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队
,等待主线程调度执行。在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行
。
例如遇到定时器像setimeout
时,浏览器交给计时线程,当定时器完成后,把定时器内的回调函数返回给消息队列
排队等待主线程的执行。
消息队列
但其实浏览器里面不只有一个队列,根据W3C 今年的消息,Event loop
取消了宏任务,但还保持了优先级最高的微队列 。 在目前的主流Chrome浏览器中可以了解到至少有以下队列(还有其它很多,这里只列举三种):
- 延时队列:用于存放计时器到达后的回调任务,优先级[中]
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级[高]
- 微队列:用户存放需要最快执行的任务,优先级[最高]
虽然W3C标准没有明确说明哪些哪些队列的优先级,但是可以自己通过事件调用的顺序大概知道谁的优先级要高一些。那么渲染主线程就是按照优先级的高 ->低
,去调用任务。
按照优先级执行fn1->fn3->fn2
先执行优先级高的队列,优先级高的队列任务执行完后,执行优先级次高的队列;当执行优先级次高的时候,如果比自己优先级高的队列又来的新的任务,则会先去执行优先级比自己高的队列的任务,如此往返循环,就叫做事件循环
代码效果:
js
funciton a(){
console.log(1);
Promise.resolve().then(function(){
console.log(2); // fn2
});
}
setTimeout(funciton () { // fn3
console.log(3);
Promise.resolve().then(a);
},0);
Promise.resolve().then(function(){//fn4
console.log(4)
})
console.log(5)
//输出结果:
5
4
3
1
2
总结
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。在chrome的源码中,它开启一个不会结束的for循环
,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列未尾即可过去把消息队列简单分为宏队列和微队列
,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。
根据w3c官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列
,微队列的任务一定具有最高的优先级,必须优先调度执行。
Node.js环境
Node.js 作为一种基于事件驱动、非阻塞式 I/O
的平台,实现了自己的事件循环机制,这一特性使得 Node.js 能够高效地处理大量 I/O 密集型任务,并使得程序的 CPU 利用率较高。
和浏览器一样,node.js的为了处理异步的API也会产生事件循环,而node.js采用的还是宏任务和微任务进行的循环体中有六个阶段,每一个阶段都有一个事件队列,不同的队列存储了不同类型的异步API的回调函数。
在宏任务和微任务的事件循环事件之上有一个特殊的异步队列Process.nextTick
优先级最高。会在事件循环直接被调用。顺序 nextTick->微任务->事件循环
- Timers阶段: 处理定时器相关的回调函数。这些回调函数会在指定的时间后执行。
- Pending I/O callbacks阶段: 处理某些系统操作(如网络请求、文件I/O等)的回调函数。
- Idle, prepare阶段: 这是一个内部使用的阶段,一般不需要关注。
- Poll阶段: 存储I/O操作的回调函数队列,比如文件读写的操作。如果事件中有回调函数,执行它们直到清空,当timer和check队列都为空时,则会阻塞在poll阶段。当timer队列有回调时,会先往下check阶段再循环下一次到timer 阶段。
- Check阶段: 执行setImmediate()的回调函数。Close callbacks阶段:处理一些关闭事件的回调函数,如socket.on('close', ...)。
- Closing callbacks: 执行与关闭事件相关,例如关闭数据库连接的回调函数等
这段六个阶段的轮回形成了Node.js的事件循环。 代码执行效果
js
const fs = require("fs")
fs.readFile('a.txt', (err, data) => {
console.log("I/O队列");
setTimeout(() => {
console.log("定时器")
}, 0)
setImmediate(() => {
console.log("node独有API");
})
})
console.log("开始");
Promise.resolve().then(()=>{
console.log("微任务队列");
})
process.nextTick(()=>{
console.log("nextTick队列");
})
setImmediate(()=>{
console.log("Check");
})
//结果是:
开始
nextTick队列
微任务队列
I/O队列
Check
定时器
node独有API