Event Loop
概念
Event Loop即事件循环,是解决javaScript单线程运行阻塞的一种机制。 主要是为了协调单线程下,事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞。
因为JavaScript 是单线程,也就是说,所有任务需要排队,前一个任务结束,才会执行后一个任务。
但是IO设备(输入、出设备)可能会因为网络等因数导致速度很慢(比如Ajax)继而CPU没有充分利用,所以设计者将IO设备的任务挂起,先执行后面的任务,等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。于是,就把所有任务分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务:
只有前一个任务执行完毕,才能执行后一个任务;直接在主线程上排队执行且最先执行,形成一个执行栈
异步任务:
不进入主线程、而是进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
一、microtask(微任务)
1、Promise.then
2、MutationObserver(Mutation Observer API 用来监视 DOM 变动)
3、Object.observe()(已废弃)
4、nextTick(Node.js 环境)
二、macrotask(宏任务)
1、script(整体代码) 注: 这个很容易忽略掉,就是指script根环境,而且它永远是第一个执行的宏任务(包含了同步的任务).
2、setTimeout
3、setInterval
4、setImmediate
5、I\O
6、UI rendering(DOM event)
执行机制:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",执行一个宏任务, 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中 宏任务执行完毕后,再依次执行执行当前微任务队列中的所有微任务,当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
(4)主线程不断重复上面的第三步。
任务队列
"任务队列"是一个先进先出的数据结构,也是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
例子1
案例来源:https://juejin.cn/post/6844903512845860872
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
1、整体代码script这段代码作为宏任务,进入主线程。
2、先遇到setTimeout,将分发到宏任务Event Queue。
3、接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
4、遇到console.log(),立即执行。
5、整体代码script作为第一个宏任务执行结束,然后会看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
6、第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
7.结束。
例子2:
setTimeout(() => {
console.log(3)
new Promise((resolve, reject) => {
console.log(5)
resolve()
}).then(console.log(6))
}, 0)
setTimeout(() => {
console.log(4)
}, 0)
new Promise((resolve, reject) => {
console.log(1)
resolve()
}).then(console.log(2))
输出是1 2 3 5 6 4
分析:
1、前面的两个setTimeout都是宏任务,所以现在宏任务队列有2个任务
2、Promise里面的代码是同步任务,所以现在会马上执行 输出1
3、Promise的then是微任务,所以现在微任务队列有1个任务
4、在执行完同步任务之后,开始执行微任务,也就是console.log(2), 输出2
5、在执行完微任务之后,会执行宏任务,第一个宏任务也就是第一个setTimeout
6、第一个setTimeout会先输出3,然后输出5,因为这两个都是同步任务,然后遇到then,加入微任务队列,宏任务执行完重新开始下一个循环。
7、因为没有同步代码,所以接着执行微任务,此时微任务队列有1个任务(第6步加入), 宏任务队列还有1个任务(第6步执行完了第一个宏任务)
8、执行微任务,输出6
9、再执行宏任务,输出4
例子3:
//主线程直接执行
console.log('1');
//丢到宏事件队列中
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
//微事件1
process.nextTick(function() {
console.log('6');
})
//主线程直接执行
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
//微事件2
console.log('8')
})
//丢到宏事件队列中
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
结果:
输出为1,7,6,8,2,4,3,5,9,11,10,12。
分析:
首先浏览器执行js进入第一个宏任务进入主线程, 直接打印console.log('1')
• 遇到 setTimeout 分发到宏任务Event Queue中
• 遇到 process.nextTick 丢到微任务Event Queue中
• 遇到 Promise, new Promise 直接执行 输出 console.log('7');
• 执行then 被分发到微任务Event Queue中
•第一轮宏任务执行结束,开始执行微任务 打印 6,8
•第一轮微任务执行完毕,执行第二轮宏事件,执行setTimeout
•先执行主线程宏任务,在执行微任务,打印'2,4,3,5'
•在执行第二个setTimeout,同理打印 ‘9,11,10,12’
•整段代码,共进行了三次事件循环
误区
nodejs事件循环和浏览器的事件循环不一样的。浏览器的Event loop是在HTML5中定义的规范,而node中则由libuv库实现
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】