学习过程中看到了一篇很不错的博客,加深了我对JS循环机制的理解事件循环机制(Event Loop)的基本认知一、什么是事件循环机制? 为什么是上面的顺序呢? 原因是JS引擎指向代码是 - 掘金
为什么需要事件循环?
JavaScript的单线程语言,这意味着他一次只能执行一个任务,但是浏览器里面进行的事务往往是多线程的。如果没有事件循环机制,当遇到耗时操作时(网络请求、文件读取等等),整个程序会被阻塞,用户界面会被“冻结”,导致极差的用户体验。
而事件循环则通过异步非阻塞的方式解决了这个问题,让单线程的JavaScript也能高效处理并发操作。
如何更好地理解事件循环
可以看下面的这个例子:
console.log('1. 开始');
setTimeout(() => {
console.log('6. setTimeout回调');
}, 0);
Promise.resolve().then(() => {
console.log('4. Promise微任务1');
}).then(() => {
console.log('5. Promise微任务2');
});
console.log('2. 中间');
new Promise(resolve => {
console.log('3. Promise构造器');
resolve();
}).then(() => {
console.log('7. Promise微任务3');
});
console.log('8. 结束');
输出结果:
1. 开始
2. 中间
3. Promise构造器
8. 结束
4. Promise微任务1
5. Promise微任务2
7. Promise微任务3
6. setTimeout回调
为什么会是这个结果呢,首先我们可以简单看一下事件循环的流程图
[调用栈] → [微任务队列] → [渲染] → [宏任务队列]
↖_________________________↙
遇到不同的任务代码,js会将其放到不同的队列/栈中,这里面包含了同步代码、微任务、宏任务
同步代码
立即执行:
- 不在任何队列中排队
- 遇到即执行,没有延迟
阻塞性:
console.log('开始');
for(let i = 0; i < 1000000000; i++) {} // 同步阻塞
console.log('结束'); // 要等待循环结束才会执行
执行上下文:
- 构成调用栈(Call Stack)的主体
- 每个函数调用都会创建一个新的执行上下文
微任务队列 (Microtask Queue)
触发时机:在当前任务执行完毕后立即执行
包含内容:
- Promise 的
.then()
/.catch()
/.finally()
回调queueMicrotask()
添加的任务- MutationObserver 回调(浏览器环境)
特点:
- 优先级第二高
- 必须清空才会进入下一阶段
- 如果微任务中又添加微任务,会继续执行直到队列为空
宏任务队列 (Macrotask Queue/Task Queue)
触发时机:每次事件循环的最后阶段
包含内容:
setTimeout
/setInterval
回调- DOM 事件回调(点击、滚动等)
- I/O 操作回调
setImmediate
(Node.js 环境)requestIdleCallback
特点:
- 每次事件循环只执行一个宏任务(新标准下可能有变化)
- 来源多样,可能有多个宏任务队列
想象你在餐厅点餐:
同步代码:你直接向厨师点餐(立即执行)
微任务:厨师完成主菜后立即添加的装饰(快速完成的小任务)
宏任务:需要等待的甜点(稍后完成的任务)
事件循环:服务员不断检查是否有新订单或已完成菜品可以上桌
厨师(JS引擎)会优先处理即时订单(同步代码),然后快速完成小装饰(微任务),最后处理需要等待的甜点(宏任务),然后又开始新一轮检查。
JS与现实世界类比
服务员(事件循环)只有一个,但需要服务多桌客人
同步点餐:立即处理的简单请求(如倒水)
微任务:快速完成的小请求(如拿餐具)
宏任务:耗时的请求(如烹饪主菜)
事件循环的核心优势
1. 非阻塞I/O处理
// 没有事件循环的"阻塞"写法(伪代码)
const data = readFileSync('largeFile.txt'); // 整个程序会停在这里等待
console.log(data);
// 有事件循环的实际写法
readFile('largeFile.txt', (err, data) => {
console.log(data); // 回调函数在文件读取完成后执行
});
console.log('我可以继续执行其他任务');
在没有事件循环的阻塞写法中,readFileSync
函数会使整个程序停在原地,等待文件读取操作完成,期间无法执行其他任何任务。而借助事件循环机制的readFile
函数,采用异步回调的方式,在文件读取的过程中,程序能够继续执行后续的console.log('我可以继续执行其他任务');
语句,实现了非阻塞 I/O 处理,大大提高了程序的执行效率和资源利用率。
2. 高响应性的用户界面
// 按钮点击处理
button.addEventListener('click', () => {
// 即使这里有个耗时操作
setTimeout(() => {
console.log('耗时操作完成');
}, 3000);
// UI仍然可以响应其他操作
});
当用户点击按钮时,即使在点击事件处理函数中设置了一个耗时 3 秒的setTimeout
操作,由于事件循环机制的存在,用户界面依然能够响应用户的其他操作,比如再次点击按钮、滚动页面等,不会因为这个耗时操作而失去响应,保证了用户界面的高响应性,提升了用户体验。